@pushpalsdev/cli 1.0.17 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/pushpals-cli.js +542 -23
  2. package/package.json +1 -1
  3. package/runtime/sandbox/apps/workerpals/.python-version +1 -0
  4. package/runtime/sandbox/apps/workerpals/Dockerfile.sandbox +71 -0
  5. package/runtime/sandbox/apps/workerpals/package.json +25 -0
  6. package/runtime/sandbox/apps/workerpals/pyproject.toml +8 -0
  7. package/runtime/sandbox/apps/workerpals/src/backends/backend_config.ts +111 -0
  8. package/runtime/sandbox/apps/workerpals/src/backends/miniswe/miniswe_executor.py +2029 -0
  9. package/runtime/sandbox/apps/workerpals/src/backends/miniswe_backend.ts +48 -0
  10. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/openai_codex_executor.py +1259 -0
  11. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex/test_openai_codex_runtime_config.py +110 -0
  12. package/runtime/sandbox/apps/workerpals/src/backends/openai_codex_backend.ts +67 -0
  13. package/runtime/sandbox/apps/workerpals/src/backends/openhands/openhands_executor.py +563 -0
  14. package/runtime/sandbox/apps/workerpals/src/backends/openhands_backend.ts +161 -0
  15. package/runtime/sandbox/apps/workerpals/src/backends/openhands_task_execute.ts +536 -0
  16. package/runtime/sandbox/apps/workerpals/src/backends/shared/executor_base.py +746 -0
  17. package/runtime/sandbox/apps/workerpals/src/backends/shared/test_settings_resolver.py +60 -0
  18. package/runtime/sandbox/apps/workerpals/src/backends/task_execute_registry.ts +21 -0
  19. package/runtime/sandbox/apps/workerpals/src/backends/types.ts +52 -0
  20. package/runtime/sandbox/apps/workerpals/src/common/execution_utils.ts +149 -0
  21. package/runtime/sandbox/apps/workerpals/src/common/executor_backend.ts +15 -0
  22. package/runtime/sandbox/apps/workerpals/src/common/generic_python_executor.ts +210 -0
  23. package/runtime/sandbox/apps/workerpals/src/common/logger.ts +65 -0
  24. package/runtime/sandbox/apps/workerpals/src/common/types.ts +9 -0
  25. package/runtime/sandbox/apps/workerpals/src/common/worktree_cleanup.ts +66 -0
  26. package/runtime/sandbox/apps/workerpals/src/context_manager.ts +45 -0
  27. package/runtime/sandbox/apps/workerpals/src/docker_executor.ts +1842 -0
  28. package/runtime/sandbox/apps/workerpals/src/execute_job.ts +3063 -0
  29. package/runtime/sandbox/apps/workerpals/src/job_runner.ts +194 -0
  30. package/runtime/sandbox/apps/workerpals/src/shell_manager.ts +210 -0
  31. package/runtime/sandbox/apps/workerpals/src/timeout_policy.ts +24 -0
  32. package/runtime/sandbox/apps/workerpals/src/workerpals_main.ts +1436 -0
  33. package/runtime/sandbox/apps/workerpals/tsconfig.json +15 -0
  34. package/runtime/sandbox/apps/workerpals/uv.lock +2014 -0
  35. package/runtime/sandbox/bun.lock +2591 -0
  36. package/runtime/sandbox/configs/backend.toml +79 -0
  37. package/runtime/sandbox/configs/default.toml +260 -0
  38. package/runtime/sandbox/configs/dev.toml +2 -0
  39. package/runtime/sandbox/configs/local.example.toml +129 -0
  40. package/runtime/sandbox/package.json +65 -0
  41. package/runtime/sandbox/packages/protocol/README.md +168 -0
  42. package/runtime/sandbox/packages/protocol/package.json +37 -0
  43. package/runtime/sandbox/packages/protocol/scripts/copy-schemas.js +17 -0
  44. package/runtime/sandbox/packages/protocol/src/a2a/README.md +52 -0
  45. package/runtime/sandbox/packages/protocol/src/a2a/mapping.ts +55 -0
  46. package/runtime/sandbox/packages/protocol/src/index.browser.ts +25 -0
  47. package/runtime/sandbox/packages/protocol/src/index.ts +25 -0
  48. package/runtime/sandbox/packages/protocol/src/schemas/approvals.schema.json +6 -0
  49. package/runtime/sandbox/packages/protocol/src/schemas/envelope.schema.json +96 -0
  50. package/runtime/sandbox/packages/protocol/src/schemas/events.schema.json +679 -0
  51. package/runtime/sandbox/packages/protocol/src/schemas/http.schema.json +50 -0
  52. package/runtime/sandbox/packages/protocol/src/types.ts +267 -0
  53. package/runtime/sandbox/packages/protocol/src/validate.browser.ts +154 -0
  54. package/runtime/sandbox/packages/protocol/src/validate.ts +233 -0
  55. package/runtime/sandbox/packages/protocol/src/version.ts +1 -0
  56. package/runtime/sandbox/packages/protocol/tsconfig.json +20 -0
  57. package/runtime/sandbox/packages/shared/package.json +19 -0
  58. package/runtime/sandbox/packages/shared/src/autonomy_policy.ts +400 -0
  59. package/runtime/sandbox/packages/shared/src/client_preflight.ts +297 -0
  60. package/runtime/sandbox/packages/shared/src/communication.ts +313 -0
  61. package/runtime/sandbox/packages/shared/src/config.ts +2201 -0
  62. package/runtime/sandbox/packages/shared/src/config_template_parity.ts +70 -0
  63. package/runtime/sandbox/packages/shared/src/git_backend.ts +205 -0
  64. package/runtime/sandbox/packages/shared/src/index.ts +100 -0
  65. package/runtime/sandbox/packages/shared/src/local_network.ts +101 -0
  66. package/runtime/sandbox/packages/shared/src/localbuddy_runtime.ts +329 -0
  67. package/runtime/sandbox/packages/shared/src/prompts.ts +64 -0
  68. package/runtime/sandbox/packages/shared/src/repo.ts +134 -0
  69. package/runtime/sandbox/packages/shared/src/session_event_visibility.ts +25 -0
  70. package/runtime/sandbox/packages/shared/src/vision.ts +247 -0
  71. package/runtime/sandbox/packages/shared/tsconfig.json +16 -0
  72. package/runtime/sandbox/prompts/workerpals/codex_quality_critic_instruction_prompt.md +14 -0
  73. package/runtime/sandbox/prompts/workerpals/commit_message_prompt.md +36 -0
  74. package/runtime/sandbox/prompts/workerpals/commit_message_user_prompt.md +7 -0
  75. package/runtime/sandbox/prompts/workerpals/miniswe_broker_system_prompt.md +33 -0
  76. package/runtime/sandbox/prompts/workerpals/miniswe_broker_task_prompt.md +5 -0
  77. package/runtime/sandbox/prompts/workerpals/miniswe_completion_requirement.md +1 -0
  78. package/runtime/sandbox/prompts/workerpals/miniswe_context_compaction_retry_prompt.md +1 -0
  79. package/runtime/sandbox/prompts/workerpals/miniswe_explicit_targets_block.md +2 -0
  80. package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_base.md +4 -0
  81. package/runtime/sandbox/prompts/workerpals/miniswe_recovery_guidance_blocker_line.md +1 -0
  82. package/runtime/sandbox/prompts/workerpals/miniswe_strict_tool_use_guidance.md +6 -0
  83. package/runtime/sandbox/prompts/workerpals/miniswe_supplemental_guidance_section.md +2 -0
  84. package/runtime/sandbox/prompts/workerpals/miniswe_timeout_note.md +1 -0
  85. package/runtime/sandbox/prompts/workerpals/miniswe_toolcall_retry_guidance.md +1 -0
  86. package/runtime/sandbox/prompts/workerpals/openai_codex_default_system_prompt.md +4 -0
  87. package/runtime/sandbox/prompts/workerpals/openai_codex_instruction_wrapper.md +5 -0
  88. package/runtime/sandbox/prompts/workerpals/openai_codex_runtime_policy_appendix.md +5 -0
  89. package/runtime/sandbox/prompts/workerpals/openai_codex_supplemental_guidance_section.md +2 -0
  90. package/runtime/sandbox/prompts/workerpals/openai_codex_task_execute_system_prompt.md +12 -0
  91. package/runtime/sandbox/prompts/workerpals/openhands_minimal_security_policy.j2 +8 -0
  92. package/runtime/sandbox/prompts/workerpals/openhands_minimal_system_prompt.j2 +20 -0
  93. package/runtime/sandbox/prompts/workerpals/openhands_strict_tool_use_message.md +1 -0
  94. package/runtime/sandbox/prompts/workerpals/openhands_supplemental_guidance_message.md +2 -0
  95. package/runtime/sandbox/prompts/workerpals/openhands_task_execute_fallback_system_prompt.md +1 -0
  96. package/runtime/sandbox/prompts/workerpals/openhands_task_execute_system_prompt.md +21 -0
  97. package/runtime/sandbox/prompts/workerpals/openhands_task_user_prompt.md +6 -0
  98. package/runtime/sandbox/prompts/workerpals/openhands_timeout_note.md +1 -0
  99. package/runtime/sandbox/prompts/workerpals/pr_description.md +42 -0
  100. package/runtime/sandbox/prompts/workerpals/task_quality_critic_system_prompt.md +9 -0
  101. package/runtime/sandbox/prompts/workerpals/task_quality_critic_user_prompt.md +17 -0
  102. package/runtime/sandbox/prompts/workerpals/workerpals_system_prompt.md +115 -0
  103. package/runtime/sandbox/protocol/schemas/approvals.schema.json +6 -0
  104. package/runtime/sandbox/protocol/schemas/envelope.schema.json +96 -0
  105. package/runtime/sandbox/protocol/schemas/events.schema.json +679 -0
  106. package/runtime/sandbox/protocol/schemas/http.schema.json +50 -0
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Docker Job Runner - Standalone job execution daemon inside Docker
4
+ *
5
+ * This script runs inside a Docker container and executes a single job.
6
+ * It's designed to be the entrypoint for sandboxed job execution.
7
+ *
8
+ * Usage (inside container):
9
+ * bun run job_runner.ts <base64-encoded-job-spec>
10
+ *
11
+ * The job spec is base64-encoded JSON: { jobId, taskId, kind, params, workerId }
12
+ *
13
+ * Output:
14
+ * stderr → JSON log lines: {"stream":"stdout|stderr","line":"..."}
15
+ * stdout → Result with sentinel: ___RESULT___ {"ok":true,...,"commit":{...}}
16
+ */
17
+
18
+ import { executeJob, shouldCommit, createJobCommit } from "./execute_job.js";
19
+ import { loadPushPalsConfig } from "shared";
20
+ import { writeFileSync } from "fs";
21
+
22
+ const CONFIG = loadPushPalsConfig();
23
+
24
+ // ─── Types ──────────────────────────────────────────────────────────────────
25
+
26
+ interface JobSpec {
27
+ jobId: string;
28
+ taskId: string;
29
+ kind: string;
30
+ params: Record<string, unknown>;
31
+ workerId: string;
32
+ }
33
+
34
+ interface JobResult {
35
+ ok: boolean;
36
+ summary: string;
37
+ stdout?: string;
38
+ stderr?: string;
39
+ exitCode?: number;
40
+ commit?: {
41
+ branch: string;
42
+ sha: string;
43
+ };
44
+ }
45
+
46
+ // ─── Logging helpers ────────────────────────────────────────────────────────
47
+
48
+ function log(stream: "stdout" | "stderr", line: string): void {
49
+ const json = JSON.stringify({ stream, line });
50
+ // eslint-disable-next-line no-console
51
+ console.error(json);
52
+ }
53
+
54
+ // ─── Git credentials setup ──────────────────────────────────────────────────
55
+
56
+ function setupGitCredentials(): void {
57
+ const token = CONFIG.gitToken ?? process.env.GIT_TOKEN ?? null;
58
+ if (!token) return;
59
+ try {
60
+ // Use a credential helper script and avoid remote URL rewriting with embedded secrets.
61
+ // This keeps push auth stable and prevents token leakage in git stderr output.
62
+ const helperScript = `#!/bin/sh
63
+ echo "username=x-access-token"
64
+ echo "password=${token}"
65
+ `;
66
+ const helperPath = "/tmp/git-credential-helper";
67
+ writeFileSync(helperPath, helperScript, { mode: 0o755 });
68
+
69
+ // Remove any legacy URL rewrite rules that may have embedded token credentials.
70
+ const urlRules = Bun.spawnSync(["git", "config", "--global", "--get-regexp", "^url\\..*\\.insteadOf$"], {
71
+ stdout: "pipe",
72
+ stderr: "pipe",
73
+ });
74
+ if (urlRules.exitCode === 0) {
75
+ const lines = String(urlRules.stdout ?? "")
76
+ .split(/\r?\n/)
77
+ .map((line) => line.trim())
78
+ .filter(Boolean);
79
+ for (const line of lines) {
80
+ const key = line.split(/\s+/, 1)[0] ?? "";
81
+ if (!key) continue;
82
+ const lower = key.toLowerCase();
83
+ if (!lower.startsWith("url.")) continue;
84
+ if (!lower.endsWith(".insteadof")) continue;
85
+ if (!lower.includes("oauth2") && !lower.includes("%3a//")) continue;
86
+ Bun.spawnSync(["git", "config", "--global", "--unset-all", key], {
87
+ stdout: "pipe",
88
+ stderr: "pipe",
89
+ });
90
+ }
91
+ }
92
+
93
+ Bun.spawnSync(["git", "config", "--global", "credential.helper", helperPath]);
94
+ } catch (err) {
95
+ log("stderr", `Failed to setup git credentials: ${err}`);
96
+ }
97
+ }
98
+
99
+ // ─── Main ───────────────────────────────────────────────────────────────────
100
+
101
+ async function main(): Promise<void> {
102
+ const args = process.argv.slice(2);
103
+ const base64Spec = args[0];
104
+
105
+ if (!base64Spec) {
106
+ // eslint-disable-next-line no-console
107
+ console.error("Usage: bun run job_runner.ts <base64-encoded-job-spec>");
108
+ process.exit(1);
109
+ }
110
+
111
+ // Decode base64 job spec
112
+ let spec: JobSpec;
113
+ try {
114
+ const json = Buffer.from(base64Spec, "base64").toString("utf-8");
115
+ spec = JSON.parse(json);
116
+ } catch (err) {
117
+ // eslint-disable-next-line no-console
118
+ console.error(`Failed to decode job spec: ${err}`);
119
+ process.exit(1);
120
+ }
121
+ log("stdout", `[JobRunner] Starting job ${spec.jobId} (${spec.kind})`);
122
+ // Setup git credentials for pushing
123
+ setupGitCredentials();
124
+ // Execute inside the mounted job worktree (docker -w), not the baked image copy.
125
+ const jobRepo = process.cwd();
126
+ const result = await executeJob(
127
+ spec.kind,
128
+ spec.params,
129
+ jobRepo,
130
+ (stream, line) => {
131
+ log(stream, line);
132
+ },
133
+ CONFIG,
134
+ );
135
+ // Build result object
136
+ const jobResult: JobResult = {
137
+ ok: result.ok,
138
+ summary: result.summary,
139
+ stdout: result.stdout,
140
+ stderr: result.stderr,
141
+ exitCode: result.exitCode,
142
+ };
143
+ // Create commit for file-modifying jobs
144
+ if (result.ok && shouldCommit(spec.kind, CONFIG)) {
145
+ log("stdout", `[JobRunner] Job modified files, creating commit...`);
146
+ const commitResult = await createJobCommit(
147
+ jobRepo,
148
+ spec.workerId,
149
+ {
150
+ id: spec.jobId,
151
+ taskId: spec.taskId,
152
+ kind: spec.kind,
153
+ params: spec.params,
154
+ context: "docker",
155
+ },
156
+ CONFIG,
157
+ );
158
+
159
+ if (commitResult.ok && commitResult.sha && commitResult.branch) {
160
+ jobResult.commit = {
161
+ branch: commitResult.branch!,
162
+ sha: commitResult.sha,
163
+ };
164
+ if (commitResult.sha === "no-changes") {
165
+ log("stdout", `[JobRunner] No changes to commit for ${spec.jobId}`);
166
+ } else {
167
+ log("stdout", `[JobRunner] Created commit ${commitResult.sha} on ${commitResult.branch}`);
168
+ }
169
+ } else {
170
+ const commitError =
171
+ commitResult.error ??
172
+ `Commit metadata missing for ${spec.kind} (${spec.jobId}) while running in Docker mode`;
173
+ jobResult.ok = false;
174
+ jobResult.summary = `Failed to create commit for ${spec.kind}`;
175
+ jobResult.stderr = [jobResult.stderr, commitError].filter(Boolean).join("\n");
176
+ jobResult.exitCode = jobResult.exitCode && jobResult.exitCode !== 0 ? jobResult.exitCode : 1;
177
+ log("stderr", `[JobRunner] Failed to create commit: ${commitError}`);
178
+ }
179
+ }
180
+
181
+ // Output result with sentinel
182
+ const resultJson = JSON.stringify(jobResult);
183
+ // eslint-disable-next-line no-console
184
+ console.log(`___RESULT___ ${resultJson}`);
185
+
186
+ // Exit with appropriate code
187
+ process.exit(jobResult.exitCode ?? (jobResult.ok ? 0 : 1));
188
+ }
189
+
190
+ main().catch((err) => {
191
+ // eslint-disable-next-line no-console
192
+ console.error(`[JobRunner] Fatal error: ${err}`);
193
+ process.exit(1);
194
+ });
@@ -0,0 +1,210 @@
1
+ // Persistent ShellManager for multi-worker shell sessions
2
+ // Milestone 1: Correctness, lease management, command framing, audit logging
3
+
4
+ import { spawn } from "bun";
5
+ import Database from "bun:sqlite";
6
+ import { randomUUID } from "crypto";
7
+ import { ContextManager } from "./context_manager";
8
+
9
+ const SESSION_TTL_MS = 10 * 60 * 1000; // 10 min
10
+ const LEASE_DURATION_MS = 60 * 1000; // 1 min
11
+
12
+ export interface ShellCommand {
13
+ cmdId: string;
14
+ command: string;
15
+ status: "pending" | "running" | "completed" | "crashed" | "killed";
16
+ exitCode?: number;
17
+ stdout?: string;
18
+ stderr?: string;
19
+ startedAt?: number;
20
+ completedAt?: number;
21
+ }
22
+
23
+ export class ShellManager {
24
+ private db: Database;
25
+ private workerId: string;
26
+ private sessions = new Map<string, ShellSessionRuntime>();
27
+ private contextMgr: ContextManager;
28
+
29
+ constructor(db: Database, workerId: string) {
30
+ this.db = db;
31
+ this.workerId = workerId;
32
+ this.contextMgr = new ContextManager(db);
33
+ this.ensureTables();
34
+ }
35
+
36
+ ensureTables() {
37
+ this.db.run(`CREATE TABLE IF NOT EXISTS shell_sessions (
38
+ session_id TEXT PRIMARY KEY,
39
+ worker_id TEXT,
40
+ lease_expiry INTEGER,
41
+ last_used_at INTEGER,
42
+ last_known_cwd TEXT,
43
+ status TEXT
44
+ )`);
45
+ this.db.run(`CREATE TABLE IF NOT EXISTS shell_commands (
46
+ cmd_id TEXT PRIMARY KEY,
47
+ session_id TEXT,
48
+ command TEXT,
49
+ status TEXT,
50
+ exit_code INTEGER,
51
+ stdout TEXT,
52
+ stderr TEXT,
53
+ started_at INTEGER,
54
+ completed_at INTEGER
55
+ )`);
56
+ }
57
+
58
+ acquireLease(sessionId: string): boolean {
59
+ const now = Date.now();
60
+ const expiry = now + LEASE_DURATION_MS;
61
+ // Try to acquire lease
62
+ this.db.run(
63
+ `INSERT OR IGNORE INTO shell_sessions (session_id, worker_id, lease_expiry, last_used_at, status) VALUES (?, ?, ?, ?, ?)`,
64
+ [sessionId, this.workerId, expiry, now, "active"],
65
+ );
66
+ const row = this.db
67
+ .query(`SELECT worker_id, lease_expiry FROM shell_sessions WHERE session_id = ?`)
68
+ .get(sessionId) as { worker_id: string; lease_expiry: number } | null;
69
+ if (row && (row.worker_id === this.workerId || row.lease_expiry < now)) {
70
+ this.db.run(
71
+ `UPDATE shell_sessions SET worker_id = ?, lease_expiry = ?, last_used_at = ?, status = ? WHERE session_id = ?`,
72
+ [this.workerId, expiry, now, "active", sessionId],
73
+ );
74
+ return true;
75
+ }
76
+ return false;
77
+ }
78
+
79
+ getOrCreate(sessionId: string): ShellSessionRuntime | null {
80
+ if (!this.acquireLease(sessionId)) return null;
81
+ let runtime = this.sessions.get(sessionId);
82
+ if (!runtime) {
83
+ // Restore last_known_cwd from context
84
+ const cwd = this.contextMgr.get(sessionId, "last_known_cwd") || process.cwd();
85
+ runtime = new ShellSessionRuntime(sessionId, this.workerId, this.db, this.contextMgr, cwd);
86
+ this.sessions.set(sessionId, runtime);
87
+ }
88
+ return runtime;
89
+ }
90
+
91
+ cleanupIdleSessions() {
92
+ const now = Date.now();
93
+ for (const [sessionId, runtime] of this.sessions) {
94
+ if (now - runtime.lastUsedAt > SESSION_TTL_MS) {
95
+ runtime.terminate();
96
+ this.sessions.delete(sessionId);
97
+ this.db.run(`UPDATE shell_sessions SET status = ? WHERE session_id = ?`, [
98
+ "stopped",
99
+ sessionId,
100
+ ]);
101
+ }
102
+ }
103
+ }
104
+
105
+ startContextRefresh(intervalMs = 10000) {
106
+ setInterval(() => {
107
+ for (const sessionId of this.sessions.keys()) {
108
+ // Refresh context for this session
109
+ const context = this.contextMgr.getAll(sessionId);
110
+ // Feed context to agent if handler exists
111
+ const session = this.sessions.get(sessionId);
112
+ if (session && typeof session.onContextRefresh === "function") {
113
+ session.onContextRefresh(context);
114
+ }
115
+ }
116
+ }, intervalMs);
117
+ }
118
+ }
119
+
120
+ export class ShellSessionRuntime {
121
+ private sessionId: string;
122
+ private workerId: string;
123
+ private db: Database;
124
+ private shellProc: ReturnType<typeof spawn> | null = null;
125
+ private commandQueue: ShellCommand[] = [];
126
+ public lastUsedAt: number = Date.now();
127
+ public cwd: string = process.cwd();
128
+ private running: boolean = false;
129
+ private contextMgr: ContextManager;
130
+ public onContextRefresh?: (context: Record<string, string>) => void;
131
+
132
+ constructor(
133
+ sessionId: string,
134
+ workerId: string,
135
+ db: Database,
136
+ contextMgr: ContextManager,
137
+ cwd: string,
138
+ ) {
139
+ this.sessionId = sessionId;
140
+ this.workerId = workerId;
141
+ this.db = db;
142
+ this.contextMgr = contextMgr;
143
+ this.cwd = cwd;
144
+ this.spawnShell();
145
+ }
146
+
147
+ spawnShell() {
148
+ // Spawn shell process (PowerShell or bash)
149
+ const isWindows = process.platform === "win32";
150
+ const shell = isWindows ? "powershell.exe" : "bash";
151
+ this.shellProc = spawn([shell], {
152
+ cwd: this.cwd,
153
+ stdin: "pipe",
154
+ stdout: "pipe",
155
+ stderr: "pipe",
156
+ });
157
+ // Attach parser, etc.
158
+ }
159
+
160
+ terminate() {
161
+ if (this.shellProc) {
162
+ this.shellProc.kill();
163
+ this.shellProc = null;
164
+ }
165
+ }
166
+
167
+ enqueueCommand(command: string) {
168
+ const cmdId = randomUUID();
169
+ const shellCmd: ShellCommand = {
170
+ cmdId,
171
+ command,
172
+ status: "pending",
173
+ startedAt: Date.now(),
174
+ };
175
+ this.commandQueue.push(shellCmd);
176
+ const startedAt = shellCmd.startedAt ?? Date.now();
177
+ this.db.run(
178
+ `INSERT INTO shell_commands (cmd_id, session_id, command, status, started_at) VALUES (?, ?, ?, ?, ?)`,
179
+ [cmdId, this.sessionId, command, "pending", startedAt],
180
+ );
181
+ this.processQueue();
182
+ return cmdId;
183
+ }
184
+
185
+ async processQueue() {
186
+ if (this.running || this.commandQueue.length === 0) return;
187
+ this.running = true;
188
+ const cmd = this.commandQueue.shift();
189
+ if (!cmd || !this.shellProc) {
190
+ this.running = false;
191
+ return;
192
+ }
193
+ // Framing: BEGIN/EXIT/CWD/END
194
+ const framed = `echo BEGIN ${cmd.cmdId}\n${cmd.command}\necho EXIT ${cmd.cmdId} $?\necho CWD ${cmd.cmdId} $(pwd)\necho END ${cmd.cmdId}`;
195
+ const stdin = this.shellProc.stdin;
196
+ if (!stdin || typeof stdin === "number" || typeof stdin.write !== "function") {
197
+ this.running = false;
198
+ return;
199
+ }
200
+ stdin.write(framed + "\n");
201
+ // TODO: parse output, update db, handle exit/cwd
202
+ // TODO: renew lease, update last_used_at
203
+ // On command completion, update last_known_cwd in context
204
+ if (this.cwd) {
205
+ this.contextMgr.set(this.sessionId, "last_known_cwd", this.cwd);
206
+ }
207
+ this.running = false;
208
+ this.processQueue();
209
+ }
210
+ }
@@ -0,0 +1,24 @@
1
+ export const DEFAULT_OPENHANDS_TIMEOUT_MS = 1_800_000;
2
+ export const DEFAULT_DOCKER_TIMEOUT_MS = 1_860_000;
3
+
4
+ export function parseOpenHandsTimeoutMs(raw: string | undefined): number {
5
+ const parsed = parseInt(raw ?? "", 10);
6
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_OPENHANDS_TIMEOUT_MS;
7
+ return Math.max(10_000, parsed);
8
+ }
9
+
10
+ export function parseDockerTimeoutMs(raw: string | undefined): number {
11
+ const parsed = parseInt(raw ?? "", 10);
12
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_DOCKER_TIMEOUT_MS;
13
+ return Math.max(10_000, parsed);
14
+ }
15
+
16
+ export function computeTimeoutWarningWindow(timeoutMs: number): {
17
+ leadMs: number;
18
+ delayMs: number;
19
+ } {
20
+ const normalized = Math.max(10_000, Math.floor(timeoutMs));
21
+ const leadMs = Math.min(60_000, Math.max(10_000, normalized - 5_000));
22
+ const delayMs = Math.max(1_000, normalized - leadMs);
23
+ return { leadMs, delayMs };
24
+ }