@the-open-engine/zeroshot 6.31.2 → 6.32.0

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.
@@ -151,21 +151,29 @@ class StatusFooter {
151
151
  }
152
152
 
153
153
  /**
154
- * Print text to stdout, coordinating with the render cycle.
155
- * When a render is in progress, queues output to prevent cursor corruption.
156
- * When no render is active, writes immediately.
157
- *
158
- * MUST be used instead of console.log() when status footer is active.
159
- * @param {string} text - Text to print (newline will be added)
154
+ * Print one logical line while coordinating with footer rendering.
155
+ * The caller owns line normalization; this method appends one newline.
156
+ * @param {string} text
160
157
  */
161
158
  print(text) {
159
+ this._queueOrWrite(`${text}\n`);
160
+ }
161
+
162
+ /**
163
+ * Write an exact streaming chunk while coordinating with footer rendering.
164
+ * @param {string} text
165
+ */
166
+ write(text) {
167
+ this._queueOrWrite(String(text));
168
+ }
169
+
170
+ /** @private */
171
+ _queueOrWrite(text) {
162
172
  if (this.isRendering) {
163
- // Queue for later - render() will flush after restoring cursor
164
173
  this.printQueue.push(text);
165
- } else {
166
- // Write immediately - no render in progress
167
- process.stdout.write(text + '\n');
174
+ return;
168
175
  }
176
+ process.stdout.write(text);
169
177
  }
170
178
 
171
179
  /**
@@ -176,8 +184,7 @@ class StatusFooter {
176
184
  _flushPrintQueue() {
177
185
  if (this.printQueue.length === 0) return;
178
186
 
179
- // Write all queued output
180
- const output = this.printQueue.map((text) => text + '\n').join('');
187
+ const output = this.printQueue.join('');
181
188
  this.printQueue = [];
182
189
  process.stdout.write(output);
183
190
  }
@@ -1,97 +1,109 @@
1
1
  import chalk from 'chalk';
2
+ import { resolveEffectiveTaskStatus } from '../effective-status.js';
2
3
  import { loadTasks } from '../store.js';
3
- import { isProcessRunning } from '../runner.js';
4
4
 
5
- export function listTasks(options = {}) {
6
- const tasks = loadTasks();
7
- const taskList = Object.values(tasks);
5
+ const DEFAULT_LIMIT = 20;
6
+
7
+ function selectTasks(options = {}, deps = {}) {
8
+ const readTasks = deps.loadTasks || loadTasks;
9
+ const resolveStatus = deps.resolveEffectiveTaskStatus || resolveEffectiveTaskStatus;
10
+ const allTasks = Object.values(readTasks());
11
+ const selected = allTasks
12
+ .map((task) => ({ task, effectiveStatus: resolveStatus(task) }))
13
+ .sort((left, right) => new Date(left.task.createdAt) - new Date(right.task.createdAt))
14
+ .filter(({ effectiveStatus }) => !options.status || effectiveStatus.status === options.status)
15
+ .slice(0, options.limit || DEFAULT_LIMIT);
16
+ return { total: allTasks.length, selected };
17
+ }
18
+
19
+ function projectTask({ task, effectiveStatus }) {
20
+ return {
21
+ id: task.id,
22
+ status: effectiveStatus.status,
23
+ statusReason: effectiveStatus.reason,
24
+ cwd: task.cwd,
25
+ provider: task.provider || null,
26
+ model: task.model || null,
27
+ createdAt: task.createdAt,
28
+ updatedAt: task.updatedAt,
29
+ exitCode: task.exitCode ?? null,
30
+ error: task.error || null,
31
+ attachable: task.attachable === true,
32
+ };
33
+ }
34
+
35
+ export function getTasksData(options = {}, deps = {}) {
36
+ return selectTasks(options, deps).selected.map(projectTask);
37
+ }
8
38
 
9
- if (taskList.length === 0) {
39
+ export function listTasks(options = {}, deps = {}) {
40
+ const { selected, total } = selectTasks(options, deps);
41
+
42
+ if (total === 0) {
10
43
  console.log(chalk.dim('No tasks found.'));
11
44
  return;
12
45
  }
13
46
 
14
- // Sort by creation date, oldest first (chronological)
15
- taskList.sort((a, b) => new Date(a.createdAt) - new Date(b.createdAt));
16
-
17
- // Filter by status if specified
18
- let filtered = taskList;
19
- if (options.status) {
20
- filtered = taskList.filter((t) => t.status === options.status);
47
+ if (options.verbose) {
48
+ printVerboseTasks(selected, total);
49
+ } else {
50
+ printTaskTable(selected, total);
21
51
  }
52
+ }
22
53
 
23
- // Limit results
24
- const limit = options.limit || 20;
25
- filtered = filtered.slice(0, limit);
26
-
27
- // Table format (default) or verbose format
28
- if (options.verbose) {
29
- // Verbose format (old behavior)
30
- console.log(chalk.bold(`\nTasks (${filtered.length}/${taskList.length})\n`));
31
-
32
- for (const task of filtered) {
33
- // Verify running status
34
- let status = task.status;
35
- if (status === 'running' && !isProcessRunning(task.pid)) {
36
- status = 'stale';
37
- }
38
-
39
- const statusColor =
40
- {
41
- running: chalk.green,
42
- completed: chalk.green,
43
- failed: chalk.red,
44
- stale: chalk.yellow,
45
- }[status] || chalk.dim;
46
-
47
- const age = getAge(task.createdAt);
48
- const timestamp = new Date(task.createdAt).toLocaleString();
49
-
50
- console.log(
51
- `${statusColor('●')} ${chalk.cyan(task.id)} ${statusColor(`[${status}]`)} ${chalk.dim(age + ' • ' + timestamp)}`
52
- );
53
- console.log(` ${chalk.dim('CWD:')} ${task.cwd}`);
54
- console.log(` ${chalk.dim('Prompt:')} ${task.prompt}`);
55
- if (task.pid && status === 'running') {
56
- console.log(` ${chalk.dim('PID:')} ${task.pid}`);
57
- }
58
- if (task.error) {
59
- console.log(` ${chalk.red('Error:')} ${task.error}`);
60
- }
61
- console.log();
54
+ function printVerboseTasks(selected, total) {
55
+ console.log(chalk.bold(`\nTasks (${selected.length}/${total})\n`));
56
+
57
+ for (const { task, effectiveStatus } of selected) {
58
+ const statusColor = colorForStatus(effectiveStatus.status);
59
+ const age = getAge(task.createdAt);
60
+ const timestamp = new Date(task.createdAt).toLocaleString();
61
+
62
+ const heading = `${statusColor('●')} ${chalk.cyan(task.id)}`;
63
+ const status = statusColor(`[${effectiveStatus.status}]`);
64
+ const timing = chalk.dim(age + ' • ' + timestamp);
65
+ console.log(`${heading} ${status} ${timing}`);
66
+ console.log(` ${chalk.dim('CWD:')} ${task.cwd}`);
67
+ console.log(` ${chalk.dim('Prompt:')} ${task.prompt}`);
68
+ if (task.pid && effectiveStatus.status === 'running') {
69
+ console.log(` ${chalk.dim('PID:')} ${task.pid}`);
62
70
  }
63
- } else {
64
- // Table format (clean, default)
65
- console.log(chalk.bold(`\n=== Tasks (${filtered.length}/${taskList.length}) ===`));
66
- console.log(`${'ID'.padEnd(25)} ${'Status'.padEnd(12)} ${'Age'.padEnd(10)} CWD`);
67
- console.log('-'.repeat(100));
68
-
69
- for (const task of filtered) {
70
- // Verify running status
71
- let status = task.status;
72
- if (status === 'running' && !isProcessRunning(task.pid)) {
73
- status = 'stale';
74
- }
75
-
76
- const statusColor =
77
- {
78
- running: chalk.green,
79
- completed: chalk.green,
80
- failed: chalk.red,
81
- stale: chalk.yellow,
82
- }[status] || chalk.dim;
83
-
84
- const age = getAge(task.createdAt);
85
- const cwd = task.cwd.replace(process.env.HOME, '~');
86
-
87
- console.log(
88
- `${chalk.cyan(task.id.padEnd(25))} ${statusColor(status.padEnd(12))} ${chalk.dim(age.padEnd(10))} ${chalk.dim(cwd)}`
89
- );
71
+ if (task.error) {
72
+ console.log(` ${chalk.red('Error:')} ${task.error}`);
90
73
  }
91
74
  console.log();
92
75
  }
93
76
  }
94
77
 
78
+ function printTaskTable(selected, total) {
79
+ console.log(chalk.bold(`\n=== Tasks (${selected.length}/${total}) ===`));
80
+ console.log(`${'ID'.padEnd(25)} ${'Status'.padEnd(12)} ${'Age'.padEnd(10)} CWD`);
81
+ console.log('-'.repeat(100));
82
+
83
+ for (const { task, effectiveStatus } of selected) {
84
+ const statusColor = colorForStatus(effectiveStatus.status);
85
+ const age = getAge(task.createdAt);
86
+ const cwd = process.env.HOME ? task.cwd.replace(process.env.HOME, '~') : task.cwd;
87
+
88
+ const id = chalk.cyan(task.id.padEnd(25));
89
+ const status = statusColor(effectiveStatus.status.padEnd(12));
90
+ const timing = chalk.dim(age.padEnd(10));
91
+ console.log(`${id} ${status} ${timing} ${chalk.dim(cwd)}`);
92
+ }
93
+ console.log();
94
+ }
95
+
96
+ function colorForStatus(status) {
97
+ return (
98
+ {
99
+ running: chalk.green,
100
+ completed: chalk.green,
101
+ failed: chalk.red,
102
+ stale: chalk.yellow,
103
+ }[status] || chalk.dim
104
+ );
105
+ }
106
+
95
107
  function getAge(dateStr) {
96
108
  const diff = Date.now() - new Date(dateStr).getTime();
97
109
  const mins = Math.floor(diff / 60000);
@@ -1,58 +1,115 @@
1
1
  import chalk from 'chalk';
2
+ import { resolveEffectiveTaskStatus } from '../effective-status.js';
2
3
  import { getTask } from '../store.js';
3
- import { isOwnedProcessTreeRunning } from '../runner.js';
4
4
 
5
- export function showStatus(taskId) {
6
- const task = getTask(taskId);
5
+ function nullable(value) {
6
+ if (value === undefined || value === null || value === '') return null;
7
+ return value;
8
+ }
9
+
10
+ function preferredPrompt(task) {
11
+ const fullPrompt = nullable(task.fullPrompt);
12
+ if (fullPrompt !== null) return fullPrompt;
13
+ return nullable(task.prompt);
14
+ }
15
+
16
+ function cleanupState(commandCleanup) {
17
+ if (commandCleanup) return 'pending';
18
+ return 'complete';
19
+ }
20
+
21
+ function projectStatus(task, effectiveStatus) {
22
+ return {
23
+ id: task.id,
24
+ status: effectiveStatus.status,
25
+ statusReason: effectiveStatus.reason,
26
+ statusDetail: effectiveStatus.detail,
27
+ createdAt: task.createdAt,
28
+ updatedAt: task.updatedAt,
29
+ cwd: task.cwd,
30
+ pid: nullable(task.pid),
31
+ exitCode: nullable(task.exitCode),
32
+ sessionId: nullable(task.sessionId),
33
+ requestedResumeSessionId: nullable(task.requestedResumeSessionId),
34
+ cleanup: cleanupState(task.commandCleanup),
35
+ logFile: nullable(task.logFile),
36
+ prompt: preferredPrompt(task),
37
+ error: nullable(task.error),
38
+ provider: nullable(task.provider),
39
+ model: nullable(task.model),
40
+ attachable: task.attachable === true,
41
+ };
42
+ }
7
43
 
44
+ export function getStatusData(taskId, deps = {}) {
45
+ const readTask = deps.getTask || getTask;
46
+ const resolveStatus = deps.resolveEffectiveTaskStatus || resolveEffectiveTaskStatus;
47
+ const task = readTask(taskId);
8
48
  if (!task) {
9
- console.log(chalk.red(`Task not found: ${taskId}`));
10
- process.exit(1);
49
+ throw new Error(`Task not found: ${taskId}`);
11
50
  }
51
+ return projectStatus(task, resolveStatus(task));
52
+ }
12
53
 
13
- // Verify running status
14
- let status = task.status;
15
- if (status === 'running') {
16
- try {
17
- const running = isOwnedProcessTreeRunning(task.pid, {
18
- processGroupId: task.processGroupId,
19
- terminationStrategy: task.terminationStrategy || 'process',
20
- });
21
- if (!running) {
22
- status = 'stale (process died)';
23
- }
24
- } catch (error) {
25
- status = `stale (invalid process ownership: ${error.message})`;
26
- }
54
+ function loadStatusOrExit(taskId, deps) {
55
+ try {
56
+ return getStatusData(taskId, deps);
57
+ } catch (error) {
58
+ console.log(chalk.red(error.message));
59
+ process.exit(1);
60
+ return null;
27
61
  }
62
+ }
28
63
 
29
- const statusColor =
64
+ function colorForStatus(status) {
65
+ return (
30
66
  {
31
67
  running: chalk.green,
32
68
  completed: chalk.green,
33
69
  failed: chalk.red,
34
- }[task.status] || chalk.yellow;
35
-
36
- console.log(chalk.bold(`\nTask: ${task.id}\n`));
37
- console.log(`${chalk.dim('Status:')} ${statusColor(status)}`);
38
- console.log(`${chalk.dim('Created:')} ${task.createdAt}`);
39
- console.log(`${chalk.dim('Updated:')} ${task.updatedAt}`);
40
- console.log(`${chalk.dim('CWD:')} ${task.cwd}`);
41
- console.log(`${chalk.dim('PID:')} ${task.pid || 'N/A'}`);
42
- console.log(`${chalk.dim('Exit Code:')} ${task.exitCode ?? 'N/A'}`);
43
- console.log(`${chalk.dim('Session:')} ${task.sessionId || 'N/A'}`);
44
- console.log(`${chalk.dim('Cleanup:')} ${task.commandCleanup ? 'pending' : 'complete'}`);
45
- if (task.requestedResumeSessionId) {
46
- console.log(`${chalk.dim('Requested:')} ${task.requestedResumeSessionId}`);
47
- }
48
- console.log(`${chalk.dim('Log File:')} ${task.logFile}`);
70
+ }[status] || chalk.yellow
71
+ );
72
+ }
49
73
 
50
- console.log(`\n${chalk.dim('Prompt:')}`);
51
- console.log(task.fullPrompt || task.prompt);
74
+ function statusLabel(status) {
75
+ if (status.statusDetail) return `${status.status} (${status.statusDetail})`;
76
+ return status.status;
77
+ }
52
78
 
53
- if (task.error) {
54
- console.log(`\n${chalk.red('Error:')} ${task.error}`);
55
- }
79
+ function displayOptional(value) {
80
+ if (value === null) return 'N/A';
81
+ return value;
82
+ }
83
+
84
+ function printRequestedSession(status) {
85
+ if (!status.requestedResumeSessionId) return;
86
+ console.log(`${chalk.dim('Requested:')} ${status.requestedResumeSessionId}`);
87
+ }
88
+
89
+ function printError(status) {
90
+ if (!status.error) return;
91
+ console.log(`\n${chalk.red('Error:')} ${status.error}`);
92
+ }
56
93
 
94
+ export function showStatus(taskId, deps = {}) {
95
+ const status = loadStatusOrExit(taskId, deps);
96
+ if (!status) return;
97
+
98
+ const statusColor = colorForStatus(status.status);
99
+ console.log(chalk.bold(`\nTask: ${status.id}\n`));
100
+ console.log(`${chalk.dim('Status:')} ${statusColor(statusLabel(status))}`);
101
+ console.log(`${chalk.dim('Created:')} ${status.createdAt}`);
102
+ console.log(`${chalk.dim('Updated:')} ${status.updatedAt}`);
103
+ console.log(`${chalk.dim('CWD:')} ${status.cwd}`);
104
+ console.log(`${chalk.dim('PID:')} ${displayOptional(status.pid)}`);
105
+ console.log(`${chalk.dim('Exit Code:')} ${displayOptional(status.exitCode)}`);
106
+ console.log(`${chalk.dim('Session:')} ${displayOptional(status.sessionId)}`);
107
+ console.log(`${chalk.dim('Cleanup:')} ${status.cleanup}`);
108
+ printRequestedSession(status);
109
+ console.log(`${chalk.dim('Log File:')} ${displayOptional(status.logFile)}`);
110
+
111
+ console.log(`\n${chalk.dim('Prompt:')}`);
112
+ console.log(displayOptional(status.prompt));
113
+ printError(status);
57
114
  console.log();
58
115
  }
@@ -0,0 +1,52 @@
1
+ import { isOwnedProcessTreeRunning } from './runner.js';
2
+
3
+ const DEFAULT_STARTUP_GRACE_MS = 30_000;
4
+
5
+ const STALE_REASON_LABELS = {
6
+ invalid_process_ownership: 'invalid process ownership',
7
+ process_died: 'process died',
8
+ startup_timeout: 'provider startup timed out',
9
+ };
10
+
11
+ function runningStatus() {
12
+ return { status: 'running', reason: null, detail: null, label: 'running' };
13
+ }
14
+
15
+ function staleStatus(reason) {
16
+ const detail = STALE_REASON_LABELS[reason];
17
+ return { status: 'stale', reason, detail, label: `stale (${detail})` };
18
+ }
19
+
20
+ function resolveStartupStatus(task, deps) {
21
+ const createdAt = Date.parse(task.createdAt);
22
+ const now = deps.now?.() ?? Date.now();
23
+ const graceMs = deps.startupGraceMs ?? DEFAULT_STARTUP_GRACE_MS;
24
+ return Number.isFinite(createdAt) && now >= createdAt && now - createdAt <= graceMs
25
+ ? runningStatus()
26
+ : staleStatus('startup_timeout');
27
+ }
28
+
29
+ export function resolveEffectiveTaskStatus(task, deps = {}) {
30
+ if (task.status !== 'running') {
31
+ return { status: task.status, reason: null, detail: null, label: task.status };
32
+ }
33
+ // The durable row is written before the detached watcher can publish its owned provider PID.
34
+ // Treat that startup window as live, but bound it so an abandoned watcher cannot look active
35
+ // indefinitely.
36
+ if (task.pid === null) return resolveStartupStatus(task, deps);
37
+
38
+ const isRunning = deps.isOwnedProcessTreeRunning || isOwnedProcessTreeRunning;
39
+ try {
40
+ if (
41
+ isRunning(task.pid, {
42
+ processGroupId: task.processGroupId,
43
+ terminationStrategy: task.terminationStrategy || 'process',
44
+ })
45
+ ) {
46
+ return runningStatus();
47
+ }
48
+ return staleStatus('process_died');
49
+ } catch {
50
+ return staleStatus('invalid_process_ownership');
51
+ }
52
+ }