From 271a83a7bc623d2c20c86aaba80fd98fc1adc09a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 8 Oct 2026 21:15:47 -0400 Subject: [PATCH] Q129: a run with failed actions it never redid does not count as completed Co-Authored-By: Claude Opus 5.5 --- .../migrations/092-run-action-targets.ts | 7 + src/main/database/schema.ts | 2 + src/main/services/agent-run-loop.ts | 11 +- src/main/services/run-journal.ts | 54 +++++++- src/main/services/task-execution-service.ts | 28 ++-- tests/integration/run-action-failures.test.ts | 128 ++++++++++++++++++ tests/unit/domain-services.test.ts | 2 +- 7 files changed, 209 insertions(+), 23 deletions(-) create mode 100644 src/main/database/migrations/092-run-action-targets.ts create mode 100644 tests/integration/run-action-failures.test.ts diff --git a/src/main/database/migrations/092-run-action-targets.ts b/src/main/database/migrations/092-run-action-targets.ts new file mode 100644 index 00000000..47fd1351 --- /dev/null +++ b/src/main/database/migrations/092-run-action-targets.ts @@ -0,0 +1,7 @@ +// What each journaled action acted on; rows from before stay null, meaning any later success of the same tool redoes them. +export const runActionTargetsSchema = ` + alter table run_actions add column target text; + + insert into schema_migrations(version, name, applied_at) + values (92, 'run_action_targets', datetime('now')); +`; diff --git a/src/main/database/schema.ts b/src/main/database/schema.ts index 174e71b2..ed80c84d 100644 --- a/src/main/database/schema.ts +++ b/src/main/database/schema.ts @@ -90,6 +90,7 @@ import { hostAccessNoteSchema } from "./migrations/088-host-access-note"; import { fileVersionsSchema } from "./migrations/089-file-versions"; import { erasureReceiptsSchema } from "./migrations/090-erasure-receipts"; import { requestPurposeSchema } from "./migrations/091-request-purpose"; +import { runActionTargetsSchema } from "./migrations/092-run-action-targets"; interface Migration { version: number; @@ -189,6 +190,7 @@ const MIGRATIONS: Migration[] = [ { version: 89, name: "file_versions", sql: fileVersionsSchema }, { version: 90, name: "erasure_receipts", sql: erasureReceiptsSchema }, { version: 91, name: "request_purpose", sql: requestPurposeSchema }, + { version: 92, name: "run_action_targets", sql: runActionTargetsSchema }, ]; const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version; diff --git a/src/main/services/agent-run-loop.ts b/src/main/services/agent-run-loop.ts index 9595b6d4..119ff51c 100644 --- a/src/main/services/agent-run-loop.ts +++ b/src/main/services/agent-run-loop.ts @@ -105,19 +105,24 @@ export async function runAgentLoop(deps: AgentRunLoopDeps, input: AgentRunLoopIn lastText = turn.text.trim() || lastText; messages.push({ role: "assistant", text: turn.text, toolCalls: turn.toolCalls, ...(turn.replay ? { replay: turn.replay } : {}) }); const results: Array<{ toolCallId: string; toolName: string; output: string }> = []; + // A call refused before it ran is still journaled, so a run that never redoes it cannot pass as completed. + const refuse = (call: ToolCall, output: string) => { + input.onToolCompleted?.(call, output); + results.push({ toolCallId: call.id, toolName: call.name, output }); + }; for (const [index, call] of turn.toolCalls.entries()) { throwIfRunStopped(); if (index >= MAX_TOOL_CALLS_PER_TURN) { - results.push({ toolCallId: call.id, toolName: call.name, output: `ERROR: not run: this turn asked for ${turn.toolCalls.length} tool calls; the limit is ${MAX_TOOL_CALLS_PER_TURN}. Repeat the ones you still need.` }); + refuse(call, `ERROR: not run: this turn asked for ${turn.toolCalls.length} tool calls; the limit is ${MAX_TOOL_CALLS_PER_TURN}. Repeat the ones you still need.`); continue; } if (call.argumentsError) { console.warn(`[run-loop] ${call.name} not run: arguments ${call.argumentsError}`); - results.push({ toolCallId: call.id, toolName: call.name, output: unreadableArguments(call) }); + refuse(call, unreadableArguments(call)); continue; } if (call.name === "complete_goal" && goalAudits >= MAX_GOAL_AUDITS_PER_SESSION) { - results.push({ toolCallId: call.id, toolName: call.name, output: `ERROR: this session already ran ${MAX_GOAL_AUDITS_PER_SESSION} goal audits. Close the gaps they named, then try again next session.` }); + refuse(call, `ERROR: this session already ran ${MAX_GOAL_AUDITS_PER_SESSION} goal audits. Close the gaps they named, then try again next session.`); continue; } if (call.name === "complete_goal") goalAudits += 1; diff --git a/src/main/services/run-journal.ts b/src/main/services/run-journal.ts index 502e1622..2f037d78 100644 --- a/src/main/services/run-journal.ts +++ b/src/main/services/run-journal.ts @@ -1,4 +1,5 @@ import type { DatabaseSync } from "node:sqlite"; +import { posix } from "node:path"; import { createId } from "../../shared/ids"; import type { ToolCall } from "./provider-tool-turn"; @@ -10,6 +11,15 @@ const READ_ONLY_TOOLS = new Set([ const JOURNAL_LINES = 40; const FIELD_CHARS = 160; const DECLINED_LINES = 5; +/** The arguments that name what an action acted on (retry controls such as confirm_not_done excluded); a tool not listed is identified by all of its arguments. */ +const TARGET_ARGUMENTS: Record = { + write_deliverable: ["path"], create_document: ["title", "doc_type"], update_document: ["doc_id"], create_task: ["title"], + update_task_status: ["task_id"], hire_agent: ["name"], grant_connector: ["agent_name", "toolkit"], revoke_connector: ["agent_name", "toolkit"], + grant_skill: ["agent_name", "skill"], revoke_skill: ["agent_name", "skill"], record_knowledge: ["topic"], revise_knowledge: ["id"], + update_goal: ["goal_id"], complete_goal: [], run_command: ["command"], composio_run_action: ["action", "account", "arguments"], +}; +/** A result line that reports failure: a refused or broken call, or a command that exited non-zero. */ +const FAILED_RESULT = (row: string) => `(${row}.result_excerpt like 'ERROR%' or (${row}.result_excerpt like 'exit code %' and ${row}.result_excerpt <> 'exit code 0'))`; /** What a held deliverable would become, over a `held_deliverables` row alias: one file, one existing document, or a new document by type and title. */ const heldTarget = (row: string) => `(case ${row}.kind when 'file' then ${row}.workspace_root || '/' || ${row}.relative_path when 'document_update' then ${row}.document_id else ${row}.doc_type || ':' || ${row}.title end)`; @@ -17,10 +27,41 @@ const heldTarget = (row: string) => `(case ${row}.kind when 'file' then ${row}.w export function recordRunAction(db: DatabaseSync, input: { orgId: string; runId: string; taskId: string; call: ToolCall; output: string }): void { if (READ_ONLY_TOOLS.has(input.call.name)) return; db.prepare(` - insert into run_actions(id, org_id, run_id, task_id, tool_name, arguments_excerpt, result_excerpt, created_at) - values (?, ?, ?, ?, ?, ?, ?, ?) + insert into run_actions(id, org_id, run_id, task_id, tool_name, arguments_excerpt, result_excerpt, target, created_at) + values (?, ?, ?, ?, ?, ?, ?, ?, ?) `).run(createId("rac"), input.orgId, input.runId, input.taskId, input.call.name, - clip(JSON.stringify(input.call.arguments)), clip(input.output.split("\n")[0] ?? ""), new Date().toISOString()); + clip(JSON.stringify(input.call.arguments)), clip(input.output.split("\n")[0] ?? ""), actionTarget(input.call), new Date().toISOString()); +} + +/** What the call acted on, or null when its arguments could not be read. */ +function actionTarget(call: ToolCall): string | null { + if (call.argumentsError) return null; + const keys = TARGET_ARGUMENTS[call.name]; + if (!keys) return JSON.stringify(canonical(call.arguments)); + return JSON.stringify(keys.map((key) => targetValue(key, call.arguments[key]))); +} + +/** An argument as the tools read it: text trimmed, paths in one spelling, nested values in one key order. */ +function targetValue(key: string, value: unknown): unknown { + if (typeof value !== "string") return canonical(value ?? null); + return key === "path" ? posix.normalize(value.trim().replaceAll("\\", "/")) : value.trim(); +} + +/** Why a session that ended normally still failed: actions that failed and were not later done successfully on the same target (any target when unknown). */ +export function unresolvedActionFailure(db: DatabaseSync, runId: string): string | null { + const failed = db.prepare(` + select a.tool_name, a.result_excerpt from run_actions a + where a.run_id = ? and ${FAILED_RESULT("a")} + and not exists ( + select 1 from run_actions later + where later.run_id = a.run_id and later.tool_name = a.tool_name and later.rowid > a.rowid and not ${FAILED_RESULT("later")} + and (a.target is null or later.target = a.target) + ) + order by a.rowid + `).all(runId) as Array<{ tool_name: string; result_excerpt: string }>; + if (failed.length === 0) return null; + const list = failed.map((action) => `${action.tool_name} (${action.result_excerpt})`).join("; "); + return `The session ended with ${failed.length} failed action${failed.length === 1 ? "" : "s"} it did not redo: ${list}`; } /** What earlier sessions (including an interrupted attempt of this run) already did on the task, and where the last one failed. */ @@ -58,3 +99,10 @@ export function runJournalLines(db: DatabaseSync, taskId: string): string[] { function clip(text: string): string { return text.length > FIELD_CHARS ? `${text.slice(0, FIELD_CHARS)}...` : text; } + +/** The same JSON value with object keys sorted, so equal arguments serialize equally. */ +function canonical(value: unknown): unknown { + if (Array.isArray(value)) return value.map(canonical); + if (value === null || typeof value !== "object") return value; + return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical((value as Record)[key])])); +} diff --git a/src/main/services/task-execution-service.ts b/src/main/services/task-execution-service.ts index 77ee68dd..379d5bbb 100644 --- a/src/main/services/task-execution-service.ts +++ b/src/main/services/task-execution-service.ts @@ -11,7 +11,7 @@ import { SkillService } from "./skill-service"; import { runAgentLoop, runTools } from "./agent-run-loop"; import type { RunModel } from "./skill-runtime"; import { combineMidRunPolls, createGoalChangePoller } from "./goal-change-poller"; -import { recordRunAction, runJournalLines } from "./run-journal"; +import { recordRunAction, unresolvedActionFailure, runJournalLines } from "./run-journal"; import { classifyProviderFailure } from "./provider-failure"; import { runStopReason, SESSION_TIME_LIMIT_MESSAGE, SESSION_TIME_LIMIT_MS, withRunControl } from "./run-control"; import type { AgentToolContext } from "./agent-tools"; @@ -145,7 +145,7 @@ export class TaskExecutionService { } catch (error) { const message = error instanceof Error ? error.message : String(error); console.error(`[task-run] run ${runId} failed before execution: ${message}`); - this.failRun(orgId, runId, null, message, false); + this.failRun(orgId, runId, null, message, null); this.audit.record({ orgId, entityType: "task_run", @@ -277,15 +277,18 @@ export class TaskExecutionService { }); if (delivered) this.notifyDataChanged(); } - this.completeRun(orgId, runId, result.lastRequestId, result.hitStepLimit); - if (task.origin === null) recordRunMemory(this.db, { orgId, runId, projectId: task.project_id, taskTitle: task.title, outcome: "completed", text: result.finalText }); + // Real work whose writes or external actions failed and were not redone has not succeeded, whatever the model said last. + const actionFailure = result.hitStepLimit || task.origin !== null ? null : unresolvedActionFailure(this.db, runId); + if (actionFailure) this.failRun(orgId, runId, result.lastRequestId, actionFailure, null); + else this.completeRun(orgId, runId, result.lastRequestId, result.hitStepLimit); + if (task.origin === null) recordRunMemory(this.db, { orgId, runId, projectId: task.project_id, taskTitle: task.title, outcome: actionFailure ? "failed" : "completed", text: actionFailure ?? result.finalText }); this.audit.record({ orgId, entityType: "task_run", entityId: runId, actorType: "system", actorId: null, - action: "completed", + action: actionFailure ? "failed" : "completed", payload: { taskId: task.id, agentId: agent.id, @@ -315,7 +318,7 @@ export class TaskExecutionService { throw new Error(stopReason); } const failure = classifyProviderFailure(error, Date.now()); - this.failRun(orgId, runId, providerRequestId, message, failure.transient); + this.failRun(orgId, runId, providerRequestId, message, failure.transient ? "transient" : null); if (failure.transient) this.cooldowns.park(provider, message, failure.retryAfterMs); else if (task.origin === null) recordRunMemory(this.db, { orgId, runId, projectId: task.project_id, taskTitle: task.title, outcome: "failed", text: message }); this.audit.record({ @@ -434,12 +437,7 @@ export class TaskExecutionService { if (!checkpoint) this.closeChatWakeTask(orgId, runId); } - /** - * A chat-wake task exists only to carry its reply run — the reply is the - * whole deliverable. The run completing therefore closes the task; left - * open it would strand forever (a task with a completed run is never - * enqueued again) and pile zombie "Reply to ..." rows onto the board. - */ + /** A chat-wake task only carries its reply run, so the run completing closes it; left open it would strand forever. */ private closeChatWakeTask(orgId: string, runId: string): void { const run = this.db.prepare("select r.task_id, t.origin from task_runs r join tasks t on t.id = r.task_id where r.id = ? and r.org_id = ?") .get(runId, orgId) as { task_id: string; origin: string | null } | undefined; @@ -479,12 +477,12 @@ export class TaskExecutionService { return runId; } - private failRun(orgId: string, runId: string, providerRequestId: string | null, errorMessage: string, transient: boolean): void { + private failRun(orgId: string, runId: string, providerRequestId: string | null, errorMessage: string, failureKind: "transient" | null): void { this.db.prepare(` update task_runs set state = ?, provider_request_id = coalesce(?, provider_request_id), completed_at = ?, error_message = ?, failure_kind = ? where id = ? and org_id = ? - `).run(TASK_RUN_STATE.FAILED, providerRequestId, new Date().toISOString(), errorMessage, transient ? "transient" : null, runId, orgId); + `).run(TASK_RUN_STATE.FAILED, providerRequestId, new Date().toISOString(), errorMessage, failureKind, runId, orgId); } private resolveSkillRuntimeSnapshot(orgId: string, agentId: string, runId: string, maxTokens: number, taskText: string, runModel: RunModel, houseStyle: boolean): SkillRuntimeSnapshotRow | null { @@ -498,6 +496,4 @@ export class TaskExecutionService { if (!snapshot) throw new Error("Skill runtime snapshot was not found."); return snapshot; } - - } diff --git a/tests/integration/run-action-failures.test.ts b/tests/integration/run-action-failures.test.ts new file mode 100644 index 00000000..1c68a8a8 --- /dev/null +++ b/tests/integration/run-action-failures.test.ts @@ -0,0 +1,128 @@ +import { randomUUID } from "node:crypto"; +import { mkdirSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; +import { DatabaseConnection } from "../../src/main/database/connection"; +import { OnboardingService } from "../../src/main/services/onboarding-service"; +import { ProviderService } from "../../src/main/services/provider-service"; +import { providerAdapters } from "../../src/main/services/provider-adapters"; +import { recordRunAction, unresolvedActionFailure } from "../../src/main/services/run-journal"; +import { TaskExecutionService } from "../../src/main/services/task-execution-service"; +import { claimNextQueuedRun } from "../../src/main/app/task-queue-worker"; +import type { AgentTurnResult, ProviderToolTurnInput, ToolCall } from "../../src/main/services/provider-tool-turn"; +import { answeringModelCheck } from "../helpers/model-check"; + +const testVault = { + encryptSecret: (secret: string) => Buffer.from(secret, "utf8"), + decryptSecret: (ciphertext: Buffer) => ciphertext.toString("utf8"), +}; + +/** A company whose kickoff run makes the given tool calls, one turn each, then ends with a final answer. */ +function company(calls: ToolCall[]) { + let turns = 0; + const turn = async (_input: ProviderToolTurnInput): Promise => { + const call = calls[turns]; + turns += 1; + return call ? { text: "Working.", toolCalls: [call], rawResponseJson: "{}" } : { text: "All done.", toolCalls: [], rawResponseJson: "{}" }; + }; + const connection = new DatabaseConnection(join(tmpdir(), `ac-action-failures-${randomUUID()}.db`)); + const providers = new ProviderService(connection.db, providerAdapters, testVault, answeringModelCheck(turn)); + const onboarding = new OnboardingService({ db: connection.db, saveProviderCredential: (input) => providers.saveCredential(input) }); + const execution = new TaskExecutionService(connection.db, providers); + const workspace = join(tmpdir(), `ac-action-failures-ws-${randomUUID()}`); + mkdirSync(workspace, { recursive: true }); + let orgId = ""; + connection.transaction(() => { + orgId = onboarding.bootstrap({ + companyName: "FlowPilot", companyDescription: "CSM analytics.", employeeName: "Yang", workspacePath: workspace, + executionMode: "ai_ceo", autoApproveHires: true, reviewDeliverables: false, + goalTitle: "Ship the MVP.", goalDescription: "A working product.", + providerKey: "openai", providerSecret: "sk-test-key", providerModel: "gpt-5.4", + }).orgId; + }); + return { connection, execution, orgId, runId: claimNextQueuedRun(connection)!.id }; +} + +const failingWrite: ToolCall = { id: "c1", name: "write_deliverable", arguments: { path: "../outside.md", content: "x" } }; + +describe("a run whose actions failed", () => { + test("does not count as completed when a failed action was never redone", async () => { + const h = company([failingWrite]); + await h.execution.executeRun(h.orgId, h.runId); + const run = h.connection.db.prepare("select state, failure_kind, error_message from task_runs where id = ?").get(h.runId) as { state: string; failure_kind: string; error_message: string }; + // Not a passing provider problem, so it counts against the task's retry budget like any failure. + expect([run.state, run.failure_kind]).toEqual(["failed", null]); + expect(run.error_message).toContain("The session ended with 1 failed action it did not redo: write_deliverable"); + h.connection.close(); + }); + + test("completes when every failed action was later done successfully", async () => { + // The tool trims titles, so " Plan " and "Plan" name the same document. + const broken: ToolCall = { id: "c1", name: "create_document", arguments: { title: " Plan ", doc_type: "plan" } }; + const fixed: ToolCall = { id: "c2", name: "create_document", arguments: { title: "Plan", doc_type: "plan", content: "The plan." } }; + const h = company([broken, fixed]); + await h.execution.executeRun(h.orgId, h.runId); + expect(h.connection.db.prepare("select state, failure_kind from task_runs where id = ?").get(h.runId)).toEqual({ state: "completed", failure_kind: null }); + h.connection.close(); + }); + + test("a success on a different target does not redo the failed one", async () => { + const broken: ToolCall = { id: "c1", name: "create_document", arguments: { title: "Required plan", doc_type: "plan" } }; + const other: ToolCall = { id: "c2", name: "create_document", arguments: { title: "Other report", doc_type: "report", content: "Body." } }; + const h = company([broken, other]); + await h.execution.executeRun(h.orgId, h.runId); + const run = h.connection.db.prepare("select state, error_message from task_runs where id = ?").get(h.runId) as { state: string; error_message: string }; + expect(run.state).toBe("failed"); + expect(run.error_message).toContain("1 failed action it did not redo: create_document"); + h.connection.close(); + }); + + test("a call whose arguments could not be read counts as a failed action", async () => { + const h = company([{ id: "c1", name: "write_deliverable", arguments: {}, argumentsError: "not valid JSON (Unexpected end): {\"path\":" }]); + await h.execution.executeRun(h.orgId, h.runId); + const run = h.connection.db.prepare("select state, error_message from task_runs where id = ?").get(h.runId) as { state: string; error_message: string }; + expect(run.state).toBe("failed"); + expect(run.error_message).toContain("1 failed action it did not redo: write_deliverable (ERROR: not run: the arguments"); + h.connection.close(); + }); + + test("a command that exited non-zero is a failure until the same command exits zero", () => { + const h = company([]); + const task = h.connection.db.prepare("select task_id from task_runs where id = ?").get(h.runId) as { task_id: string }; + const run = (command: string, output: string) => recordRunAction(h.connection.db, { orgId: h.orgId, runId: h.runId, taskId: task.task_id, call: { id: command, name: "run_command", arguments: { command } }, output }); + run("pnpm test", "exit code 1\n3 failed"); + run("git status", "exit code 0\nclean"); + expect(unresolvedActionFailure(h.connection.db, h.runId)).toBe("The session ended with 1 failed action it did not redo: run_command (exit code 1)"); + run("pnpm test", "exit code 0\nall passed"); + expect(unresolvedActionFailure(h.connection.db, h.runId)).toBeNull(); + h.connection.close(); + }); + + test("a connector action on other arguments does not redo the failed one", () => { + const h = company([]); + const task = h.connection.db.prepare("select task_id from task_runs where id = ?").get(h.runId) as { task_id: string }; + const update = (issue: number, output: string) => recordRunAction(h.connection.db, { + orgId: h.orgId, runId: h.runId, taskId: task.task_id, output, + call: { id: `i${issue}`, name: "composio_run_action", arguments: { action: "GITHUB_UPDATE_ISSUE", arguments: { issue_number: issue } } }, + }); + update(1, "ERROR: GITHUB_UPDATE_ISSUE failed: 502"); + recordRunAction(h.connection.db, { + orgId: h.orgId, runId: h.runId, taskId: task.task_id, output: "ERROR: reply lost", + call: { id: "multi", name: "composio_run_action", arguments: { action: "GITHUB_UPDATE_ISSUE", arguments: { issue_number: 3, state: "closed" } } }, + }); + recordRunAction(h.connection.db, { + orgId: h.orgId, runId: h.runId, taskId: task.task_id, output: "{\"successful\":true}", + call: { id: "multi2", name: "composio_run_action", arguments: { action: "GITHUB_UPDATE_ISSUE", arguments: { state: "closed", issue_number: 3 } } }, + }); + update(2, "{\"successful\":true}"); + expect(unresolvedActionFailure(h.connection.db, h.runId)).toBe("The session ended with 1 failed action it did not redo: composio_run_action (ERROR: GITHUB_UPDATE_ISSUE failed: 502)"); + // A retry that confirms the lost reply, with the same input in another key order, redoes issue 1. + recordRunAction(h.connection.db, { + orgId: h.orgId, runId: h.runId, taskId: task.task_id, output: "{\"successful\":true}", + call: { id: "retry", name: "composio_run_action", arguments: { arguments: { issue_number: 1 }, confirm_not_done: true, action: "GITHUB_UPDATE_ISSUE" } }, + }); + expect(unresolvedActionFailure(h.connection.db, h.runId)).toBeNull(); + h.connection.close(); + }); +}); diff --git a/tests/unit/domain-services.test.ts b/tests/unit/domain-services.test.ts index e7a2d7e9..d14f73bc 100644 --- a/tests/unit/domain-services.test.ts +++ b/tests/unit/domain-services.test.ts @@ -35,7 +35,7 @@ describe("domain services", () => { const connection = createConnection(); const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>; - expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91]); + expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92]); connection.close(); });