pi-orchestrator-config 4.2.0 → 4.3.2
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.
- package/README.md +76 -67
- package/agents/code-reviewer-docs.md +11 -1
- package/agents/code-reviewer-guidelines.md +11 -1
- package/agents/code-reviewer-quality.md +11 -1
- package/agents/code-reviewer-security.md +11 -1
- package/agents/code-reviewer-spec.md +11 -1
- package/agents/git-expert.md +42 -3
- package/agents/github-expert.md +11 -0
- package/extensions/acpx-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/agents/claude.ts +3 -2
- package/extensions/cli-provider/agents/cursor.ts +3 -5
- package/extensions/cli-provider/agents/gemini.ts +3 -2
- package/extensions/cli-provider/discover.ts +7 -7
- package/extensions/cli-provider/index.ts +5 -0
- package/extensions/cli-provider/runtime-models.ts +18 -6
- package/extensions/cli-provider/session-reaper.ts +5 -7
- package/extensions/cli-provider/sessions.ts +4 -0
- package/extensions/cli-provider/shared/discover-cache.ts +17 -176
- package/extensions/coms/coms-p2p.ts +1719 -423
- package/extensions/coms/coms-shared.ts +261 -39
- package/extensions/coms/coms-wrapper.ts +100 -62
- package/extensions/coms/index.ts +8 -4
- package/extensions/coms/mixed-turn.ts +26 -0
- package/extensions/coms/probe-socket.ts +42 -0
- package/extensions/coms/themeMap.ts +0 -1
- package/extensions/image-gen/image-gen.ts +19 -1
- package/extensions/orchestrator/async-agents.ts +563 -102
- package/extensions/orchestrator/async-capability.ts +8 -8
- package/extensions/orchestrator/cron.ts +15 -9
- package/extensions/orchestrator/dreaming.ts +32 -25
- package/extensions/orchestrator/enforcement-helpers.ts +459 -17
- package/extensions/orchestrator/enforcement-rules.ts +5 -4
- package/extensions/orchestrator/enforcement.ts +80 -27
- package/extensions/orchestrator/extended-autocomplete.ts +7 -0
- package/extensions/orchestrator/git-helpers.ts +2 -2
- package/extensions/orchestrator/index.ts +78 -10
- package/extensions/orchestrator/markdown-transformer.ts +313 -0
- package/extensions/orchestrator/memory-embeddings.ts +36 -34
- package/extensions/orchestrator/memory-provenance.ts +29 -25
- package/extensions/orchestrator/memory-scoring.ts +23 -18
- package/extensions/orchestrator/memory-tools.ts +10 -3
- package/extensions/orchestrator/overlay-dashboard.ts +17 -5
- package/extensions/orchestrator/parse-model-override.ts +29 -0
- package/extensions/orchestrator/pi-config-review-state.ts +57 -33
- package/extensions/orchestrator/project-settings.ts +333 -127
- package/extensions/orchestrator/resolve-agent-model.ts +24 -13
- package/extensions/orchestrator/review-porcelain.ts +4 -0
- package/extensions/orchestrator/review-ui.ts +63 -15
- package/extensions/orchestrator/rule-placeholders.ts +411 -2
- package/extensions/orchestrator/rules.ts +80 -132
- package/extensions/orchestrator/settings-tui-helpers.ts +380 -0
- package/extensions/orchestrator/settings-tui-submenus.ts +647 -0
- package/extensions/orchestrator/settings-tui.ts +636 -0
- package/extensions/orchestrator/state-jsonl.ts +431 -0
- package/extensions/orchestrator/subagent-tool.ts +157 -59
- package/extensions/orchestrator/sync-limit.ts +23 -0
- package/extensions/orchestrator/task-lifecycle.ts +77 -67
- package/extensions/orchestrator/utils.ts +8 -5
- package/extensions/pidash/pidash.ts +193 -156
- package/extensions/pidiff/pidiff.ts +96 -38
- package/extensions/pidiff/spawn-lock.ts +62 -0
- package/extensions/pitasks/index.ts +430 -0
- package/extensions/pitasks/reminders.ts +37 -0
- package/extensions/pitasks/task-store.ts +467 -0
- package/extensions/pitasks/task-tools.ts +375 -0
- package/extensions/pitasks/task-widget.ts +181 -0
- package/extensions/pitasks/types.ts +38 -0
- package/extensions/providers/acpx-driver.ts +12 -2
- package/extensions/providers/built-in-drivers.ts +6 -6
- package/extensions/providers/claude-driver.ts +1 -1
- package/extensions/providers/cursor-cli-driver.ts +1 -1
- package/extensions/providers/gemini-driver.ts +1 -1
- package/extensions/providers/index.ts +144 -26
- package/extensions/providers/initialized-guard.ts +27 -0
- package/extensions/providers/restore-default-model.ts +596 -0
- package/extensions/providers/session-shutdown.ts +39 -0
- package/extensions/shared/coms-active.ts +23 -0
- package/extensions/shared/create-runtime-provider.ts +2 -0
- package/extensions/shared/daemon-manager.ts +0 -8
- package/extensions/shared/file-logger.ts +132 -25
- package/extensions/shared/logger.ts +50 -0
- package/extensions/shared/models-dev.ts +421 -0
- package/extensions/shared/oneshot.ts +116 -0
- package/extensions/shared/resolve-binary.ts +191 -0
- package/extensions/shared/types.ts +3 -0
- package/package.json +9 -3
- package/prompts/coderabbit-rate-limit.md +2 -0
- package/rules/00-orchestrator-core.md +3 -0
- package/rules/10-agent-routing.md +12 -1
- package/rules/20-code-review-loop.md +52 -25
- package/rules/30-prompt-templates.md +4 -0
- package/rules/35-memory.md +3 -3
- package/rules/40-critical-rules.md +3 -0
- package/rules/55-coms-protocol.md +84 -18
- package/rules/60-task-tracking.md +2 -0
- package/settings-keys.json +563 -0
- package/templates/coms-feature-manager-prompt.md +4 -6
- package/extensions/coms/coms-net-server.ts +0 -1642
- package/extensions/coms/coms-net-wrapper.ts +0 -521
- package/extensions/coms/coms-net.ts +0 -1589
|
@@ -6,7 +6,7 @@ import { spawn } from "node:child_process";
|
|
|
6
6
|
import * as fs from "node:fs";
|
|
7
7
|
import * as os from "node:os";
|
|
8
8
|
import * as path from "node:path";
|
|
9
|
-
import { pathToFileURL } from "node:url";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
10
|
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
12
|
|
|
@@ -16,6 +16,7 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
|
16
16
|
import type { Message } from "@earendil-works/pi-ai";
|
|
17
17
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
18
18
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { createLogger } from "../shared/logger.js";
|
|
19
20
|
import {
|
|
20
21
|
getMarkdownTheme,
|
|
21
22
|
withFileMutationQueue,
|
|
@@ -29,29 +30,33 @@ import {
|
|
|
29
30
|
import { Type } from "typebox";
|
|
30
31
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
|
|
31
32
|
|
|
32
|
-
//
|
|
33
|
-
let
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for (const c of candidates) {
|
|
40
|
-
try { const mod = await import(c); if (mod.TaskStore) { TaskStoreClass = mod.TaskStore; break; } } catch { continue; }
|
|
41
|
-
}
|
|
42
|
-
if (!TaskStoreClass) {
|
|
43
|
-
throw new Error("[subagent] FATAL: TaskStore not found — @tintinweb/pi-tasks is required but failed to load");
|
|
44
|
-
}
|
|
45
|
-
})();
|
|
33
|
+
// Task store access via shared instance from extensions/pitasks
|
|
34
|
+
let _taskStore: any = null;
|
|
35
|
+
async function getTaskStore(): Promise<any> {
|
|
36
|
+
if (_taskStore) return _taskStore;
|
|
37
|
+
try { const mod = await import("../pitasks/index.js"); _taskStore = mod.taskStore; } catch { /* ignore */ }
|
|
38
|
+
return _taskStore;
|
|
39
|
+
}
|
|
46
40
|
import {
|
|
47
41
|
decideAsyncLlmDispatch,
|
|
48
42
|
supportsAsyncLlm,
|
|
49
43
|
} from "./async-capability.js";
|
|
50
44
|
import { autoMarkInProgress, autoCompleteTask } from "./async-agents.js";
|
|
45
|
+
import { getSetting } from "./project-settings.js";
|
|
46
|
+
import { checkSyncLimit } from "./sync-limit.js";
|
|
47
|
+
import { substituteSettingsPlaceholders } from "./rule-placeholders.js";
|
|
51
48
|
import { resolveAgentModelProvider } from "./resolve-agent-model.js";
|
|
49
|
+
import { parseModelOverride, mergeModelOverride } from "./parse-model-override.js";
|
|
52
50
|
import { clockHHMM, getPiInvocation, getProjectTmpDir, djb2Hash } from "./utils.js";
|
|
53
51
|
|
|
54
|
-
|
|
52
|
+
const SETTINGS_KEYS_FOR_SUBAGENT = JSON.parse(
|
|
53
|
+
fs.readFileSync(
|
|
54
|
+
path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "settings-keys.json"),
|
|
55
|
+
"utf-8",
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
export { resolveAgentModelProvider, parseModelOverride, checkSyncLimit };
|
|
55
60
|
|
|
56
61
|
// ── Constants ────────────────────────────────────────────────────────────
|
|
57
62
|
|
|
@@ -59,11 +64,19 @@ const MAX_PARALLEL_TASKS = 8;
|
|
|
59
64
|
const MAX_CONCURRENCY = 4;
|
|
60
65
|
const COLLAPSED_ITEM_COUNT = 10;
|
|
61
66
|
const MISSING_CWD_ERROR = "Missing required parameter: cwd. Always specify the working directory for subagent tasks.";
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
67
|
+
// Resolved per-call from settings
|
|
68
|
+
const log = createLogger("subagent");
|
|
69
|
+
const SYNC_TIME_EXCEEDED_ERROR = (seconds: number, limit: number, cwd: string) => {
|
|
70
|
+
log.info("sync_time_exceeded", "seconds", seconds, "limit", limit, "cwd", cwd);
|
|
71
|
+
return `Estimated time ${seconds}s meets or exceeds ${limit}s sync limit. Use async: true instead.`;
|
|
72
|
+
};
|
|
65
73
|
const MISSING_ESTIMATE_ERROR = "Missing required parameter: estimatedSeconds. Provide an estimated duration in seconds for sync agent tasks.";
|
|
66
74
|
|
|
75
|
+
let syncPenalty = 0;
|
|
76
|
+
let lastSyncAgent = "";
|
|
77
|
+
let lastSyncEstimate = 0;
|
|
78
|
+
let lastSyncActual = 0;
|
|
79
|
+
|
|
67
80
|
/** Agents that MUST be dispatched with async: true. Sync calls are rejected.
|
|
68
81
|
* Keep in sync with rules/20-code-review-loop.md when changing this list. */
|
|
69
82
|
const ASYNC_ONLY_AGENTS = new Set([
|
|
@@ -82,6 +95,7 @@ const TaskItem = Type.Object({
|
|
|
82
95
|
name: Type.Optional(Type.String({ description: "Display name for async status" })),
|
|
83
96
|
estimatedSeconds: Type.Optional(Type.Number({ description: "Estimated task duration in seconds. Required for sync parallel tasks." })),
|
|
84
97
|
taskId: Type.Optional(Type.String({ description: "Task ID to auto-complete when this async agent finishes" })),
|
|
98
|
+
model: Type.Optional(Type.String({ description: "Override the agent's model. Format: 'provider/model-id' or 'model-id'." })),
|
|
85
99
|
});
|
|
86
100
|
const ChainItem = Type.Object({
|
|
87
101
|
agent: Type.String({ description: "Agent name" }),
|
|
@@ -141,6 +155,9 @@ const SubagentParams = Type.Object({
|
|
|
141
155
|
persistSession: Type.Optional(
|
|
142
156
|
Type.Boolean({ description: "Persist agent session to disk for reuse across calls. Default: false (ephemeral). When true, the same agent in the same project reuses its session — keeps context, saves tokens." }),
|
|
143
157
|
),
|
|
158
|
+
model: Type.Optional(
|
|
159
|
+
Type.String({ description: "Override the agent's model. Format: 'provider/model-id' (e.g., 'litellm/claude-opus-4-6-1m') or just 'model-id' (inherits provider). Overrides all other model resolution." }),
|
|
160
|
+
),
|
|
144
161
|
});
|
|
145
162
|
|
|
146
163
|
// ── Interfaces ───────────────────────────────────────────────────────────
|
|
@@ -370,6 +387,7 @@ export async function runSingleAgent(
|
|
|
370
387
|
parentModelId?: string,
|
|
371
388
|
parentProvider?: string,
|
|
372
389
|
persistSession?: boolean,
|
|
390
|
+
explicit?: { model?: string; provider?: string },
|
|
373
391
|
): Promise<SingleResult> {
|
|
374
392
|
const agent = agents.find((a) => a.name === agentName);
|
|
375
393
|
if (!agent) {
|
|
@@ -401,7 +419,7 @@ export async function runSingleAgent(
|
|
|
401
419
|
// Deterministic session ID based on agent + cwd for session reuse
|
|
402
420
|
args.push("--session-id", `sync-${agentName}-${djb2Hash(agentName + ':' + cwd).toString(36)}`);
|
|
403
421
|
}
|
|
404
|
-
const { model: effectiveModel, provider: effectiveProvider } = resolveAgentModelProvider(agentName, agent, parentModelId, parentProvider, cwd);
|
|
422
|
+
const { model: effectiveModel, provider: effectiveProvider } = resolveAgentModelProvider(agentName, agent, parentModelId, parentProvider, cwd, explicit);
|
|
405
423
|
if (effectiveModel) args.push("--model", effectiveModel);
|
|
406
424
|
if (effectiveProvider) args.push("--provider", effectiveProvider);
|
|
407
425
|
if (agent.tools && agent.tools.length > 0)
|
|
@@ -446,7 +464,12 @@ export async function runSingleAgent(
|
|
|
446
464
|
|
|
447
465
|
try {
|
|
448
466
|
if (agent.systemPrompt.trim()) {
|
|
449
|
-
const
|
|
467
|
+
const resolvedPrompt = substituteSettingsPlaceholders(
|
|
468
|
+
agent.systemPrompt,
|
|
469
|
+
(key) => getSetting(cwd, key as any),
|
|
470
|
+
Object.keys(SETTINGS_KEYS_FOR_SUBAGENT),
|
|
471
|
+
);
|
|
472
|
+
const tmp = await writePromptFile(agent.name, resolvedPrompt, cwd);
|
|
450
473
|
tmpDir = tmp.dir;
|
|
451
474
|
tmpFile = tmp.filePath;
|
|
452
475
|
args.push("--append-system-prompt", tmpFile);
|
|
@@ -454,13 +477,14 @@ export async function runSingleAgent(
|
|
|
454
477
|
args.push(`Task: ${task}`);
|
|
455
478
|
let aborted = false;
|
|
456
479
|
|
|
480
|
+
log.info("sync_spawn_start", agentName, "cwd", cwd);
|
|
457
481
|
const exitCode = await new Promise<number>((resolve) => {
|
|
458
482
|
const inv = getPiInvocation(args);
|
|
459
483
|
const proc = spawn(inv.command, inv.args, {
|
|
460
484
|
cwd: cwd,
|
|
461
485
|
shell: false,
|
|
462
486
|
stdio: ["ignore", "pipe", "pipe"],
|
|
463
|
-
env: { ...process.env, PI_SUBAGENT_CHILD: "1", PI_AGENT_NAME: agentName },
|
|
487
|
+
env: { ...process.env, PI_SUBAGENT_CHILD: "1", PI_AGENT_NAME: agentName, PI_PRIMARY_MODEL: process.env.PI_PRIMARY_MODEL || process.env.PI_MODEL || "", __PI_CONFIG_SESSION_ID: (globalThis as any).__piConfigSessionId || "", __PI_PARENT_SESSION_ID: (globalThis as any).__piConfigSessionId || "" },
|
|
464
488
|
});
|
|
465
489
|
let buf = "";
|
|
466
490
|
|
|
@@ -508,10 +532,14 @@ export async function runSingleAgent(
|
|
|
508
532
|
cur.stderr += d.toString();
|
|
509
533
|
});
|
|
510
534
|
proc.on("close", (c) => {
|
|
535
|
+
log.info("sync_proc_close", agentName, "code", c);
|
|
511
536
|
if (buf.trim()) processLine(buf);
|
|
512
537
|
resolve(c ?? 0);
|
|
513
538
|
});
|
|
514
|
-
proc.on("error", () =>
|
|
539
|
+
proc.on("error", (err) => {
|
|
540
|
+
log.error("sync_proc_error", agentName, err?.message);
|
|
541
|
+
resolve(1);
|
|
542
|
+
});
|
|
515
543
|
|
|
516
544
|
if (signal) {
|
|
517
545
|
const kill = () => {
|
|
@@ -526,8 +554,10 @@ export async function runSingleAgent(
|
|
|
526
554
|
}
|
|
527
555
|
});
|
|
528
556
|
|
|
557
|
+
log.info("sync_proc_resolved", agentName, "exitCode", exitCode, "elapsed", Date.now() - startTime);
|
|
529
558
|
cur.exitCode = exitCode;
|
|
530
559
|
cur.durationMs = Date.now() - startTime;
|
|
560
|
+
|
|
531
561
|
if (aborted) throw new Error("Subagent was aborted");
|
|
532
562
|
return cur;
|
|
533
563
|
} finally {
|
|
@@ -542,32 +572,21 @@ export async function runSingleAgent(
|
|
|
542
572
|
}
|
|
543
573
|
}
|
|
544
574
|
|
|
545
|
-
/** Validate that a taskId references an existing task in the
|
|
546
|
-
function validateTaskId(taskId: string,
|
|
575
|
+
/** Validate that a taskId references an existing task in the pitasks store. */
|
|
576
|
+
function validateTaskId(taskId: string, _cwd: string, _sessionId?: string): string | null {
|
|
547
577
|
if (taskId === "-1") return null;
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
paths.push(path.join(tasksDir, "tasks.json"));
|
|
553
|
-
|
|
554
|
-
for (const p of paths) {
|
|
555
|
-
try {
|
|
556
|
-
if (TaskStoreClass) {
|
|
557
|
-
const store = new TaskStoreClass(p);
|
|
558
|
-
if (store.get(taskId)) return null;
|
|
559
|
-
} else {
|
|
560
|
-
// Fallback: raw file read when TaskStore hasn't loaded yet (async init race)
|
|
561
|
-
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
562
|
-
if ((data.tasks || []).some((t: any) => t.id === taskId)) return null;
|
|
563
|
-
}
|
|
564
|
-
} catch (e: any) {
|
|
565
|
-
// Only continue silently for missing files; report other errors
|
|
566
|
-
if (e?.code === "ENOENT" || e?.message?.includes("ENOENT")) continue;
|
|
567
|
-
return `Task store error for ${path.basename(p)}: ${e?.message || e}. Fix the task store or pass taskId: "-1".`;
|
|
578
|
+
try {
|
|
579
|
+
// Ensure store is loaded (async init may not have completed)
|
|
580
|
+
if (!_taskStore) {
|
|
581
|
+
getTaskStore().catch(() => {});
|
|
568
582
|
}
|
|
569
|
-
|
|
570
|
-
|
|
583
|
+
const store = _taskStore;
|
|
584
|
+
if (!store) {
|
|
585
|
+
// Store not loaded yet (async init) — fail open, skip validation
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
if (store.get(taskId)) return null;
|
|
589
|
+
} catch { return null; }
|
|
571
590
|
return `Task #${taskId} not found. Verify the task ID exists (use TaskList to check), or pass taskId: "-1" if not linked to a task.`;
|
|
572
591
|
}
|
|
573
592
|
|
|
@@ -575,7 +594,7 @@ function validateTaskId(taskId: string, cwd: string, sessionId?: string): string
|
|
|
575
594
|
|
|
576
595
|
export function registerSubagentTool(
|
|
577
596
|
pi: ExtensionAPI,
|
|
578
|
-
spawnAsyncAgent: (agentName: string, task: string, cwd: string, agents: AgentConfig[], options?: { fireAndForget?: boolean; name?: string; parentModelId?: string; parentProvider?: string; groupId?: string; taskId?: string }) => { id: string; error?: string; model?: string },
|
|
597
|
+
spawnAsyncAgent: (agentName: string, task: string, cwd: string, agents: AgentConfig[], options?: { fireAndForget?: boolean; name?: string; parentModelId?: string; parentProvider?: string; groupId?: string; taskId?: string; persistSession?: boolean; explicit?: { model?: string; provider?: string } }) => { id: string; error?: string; model?: string },
|
|
579
598
|
killAsyncAgent: (target: string) => { killed: string[]; errors: string[] },
|
|
580
599
|
): void {
|
|
581
600
|
// Only the orchestrator (top-level pi) can spawn subagents.
|
|
@@ -606,6 +625,7 @@ export function registerSubagentTool(
|
|
|
606
625
|
parentModelId: string | undefined,
|
|
607
626
|
parentProvider: string | undefined,
|
|
608
627
|
ctx: any,
|
|
628
|
+
explicit?: { model?: string; provider?: string },
|
|
609
629
|
) {
|
|
610
630
|
if (params.chain) {
|
|
611
631
|
return {
|
|
@@ -664,6 +684,7 @@ export function registerSubagentTool(
|
|
|
664
684
|
? `group-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
665
685
|
: undefined;
|
|
666
686
|
for (const t of params.tasks) {
|
|
687
|
+
const taskExplicit = mergeModelOverride(t.model, explicit);
|
|
667
688
|
const r = spawnAsyncAgent(t.agent, t.task, t.cwd, agents, {
|
|
668
689
|
fireAndForget: params.fireAndForget,
|
|
669
690
|
name: (t as any).name,
|
|
@@ -672,6 +693,7 @@ export function registerSubagentTool(
|
|
|
672
693
|
groupId,
|
|
673
694
|
taskId: (t as any).taskId,
|
|
674
695
|
persistSession: params.persistSession,
|
|
696
|
+
explicit: taskExplicit,
|
|
675
697
|
});
|
|
676
698
|
if (r.error) {
|
|
677
699
|
errors.push(`${t.agent}: ${r.error}`);
|
|
@@ -742,7 +764,19 @@ export function registerSubagentTool(
|
|
|
742
764
|
};
|
|
743
765
|
}
|
|
744
766
|
}
|
|
745
|
-
|
|
767
|
+
if (syncPenalty > 0) {
|
|
768
|
+
log.info("sync_penalty_reset", "async used");
|
|
769
|
+
syncPenalty = 0;
|
|
770
|
+
}
|
|
771
|
+
const result = spawnAsyncAgent(params.agent, params.task, params.cwd, agents, {
|
|
772
|
+
fireAndForget: params.fireAndForget,
|
|
773
|
+
name: params.name,
|
|
774
|
+
parentModelId,
|
|
775
|
+
parentProvider,
|
|
776
|
+
taskId: params.taskId,
|
|
777
|
+
persistSession: params.persistSession,
|
|
778
|
+
explicit: explicit,
|
|
779
|
+
});
|
|
746
780
|
if (result.error) {
|
|
747
781
|
return {
|
|
748
782
|
content: [{ type: "text" as const, text: result.error }],
|
|
@@ -770,6 +804,7 @@ export function registerSubagentTool(
|
|
|
770
804
|
parentProvider: string | undefined,
|
|
771
805
|
asyncOk: boolean,
|
|
772
806
|
sessionId?: string,
|
|
807
|
+
explicit?: { model?: string; provider?: string },
|
|
773
808
|
) {
|
|
774
809
|
const results: SingleResult[] = [];
|
|
775
810
|
let prev = "";
|
|
@@ -807,9 +842,25 @@ export function registerSubagentTool(
|
|
|
807
842
|
}
|
|
808
843
|
// chain runs sequentially — sum all steps
|
|
809
844
|
const totalChainSeconds = params.chain.reduce((sum: number, s: any) => sum + s.estimatedSeconds, 0);
|
|
810
|
-
if (
|
|
845
|
+
if (!params.chain || params.chain.length === 0) {
|
|
846
|
+
return { content: [{ type: "text" as const, text: "Error: chain array is empty — at least one step is required." }] };
|
|
847
|
+
}
|
|
848
|
+
const chainCwd = params.chain[0].cwd;
|
|
849
|
+
const { limit: chainLimit } = checkSyncLimit(0, chainCwd);
|
|
850
|
+
if (syncPenalty > 0) {
|
|
851
|
+
const adjustedTotal = totalChainSeconds + syncPenalty;
|
|
852
|
+
if (adjustedTotal >= chainLimit) {
|
|
853
|
+
log.info("sync_penalty_rejected_chain", "penalty", syncPenalty, "adjusted", adjustedTotal);
|
|
854
|
+
return {
|
|
855
|
+
content: [{ type: "text" as const, text: `Rejected: your last sync agent '${lastSyncAgent}' estimated ${lastSyncEstimate}s but took ${lastSyncActual}s. Penalty: ${syncPenalty}s. Chain total ${totalChainSeconds}s + penalty ${syncPenalty}s = ${adjustedTotal}s — exceeds ${chainLimit}s limit. Use async: true.` }],
|
|
856
|
+
details: mkd("chain")([]),
|
|
857
|
+
isError: true,
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (totalChainSeconds >= chainLimit && asyncOk) {
|
|
811
862
|
return {
|
|
812
|
-
content: [{ type: "text" as const, text: SYNC_TIME_EXCEEDED_ERROR(totalChainSeconds) }],
|
|
863
|
+
content: [{ type: "text" as const, text: SYNC_TIME_EXCEEDED_ERROR(totalChainSeconds, chainLimit, chainCwd) }],
|
|
813
864
|
details: mkd("chain")([]),
|
|
814
865
|
isError: true,
|
|
815
866
|
};
|
|
@@ -848,6 +899,7 @@ export function registerSubagentTool(
|
|
|
848
899
|
parentModelId,
|
|
849
900
|
parentProvider,
|
|
850
901
|
params.persistSession,
|
|
902
|
+
explicit,
|
|
851
903
|
);
|
|
852
904
|
activeAgents.delete(s.agent);
|
|
853
905
|
updateWorking();
|
|
@@ -899,6 +951,7 @@ export function registerSubagentTool(
|
|
|
899
951
|
parentProvider: string | undefined,
|
|
900
952
|
asyncOk: boolean,
|
|
901
953
|
sessionId?: string,
|
|
954
|
+
explicit?: { model?: string; provider?: string },
|
|
902
955
|
) {
|
|
903
956
|
if (params.tasks.length > MAX_PARALLEL_TASKS)
|
|
904
957
|
return {
|
|
@@ -930,9 +983,22 @@ export function registerSubagentTool(
|
|
|
930
983
|
}
|
|
931
984
|
// parallel runs concurrently — use longest task
|
|
932
985
|
const maxParallelSeconds = Math.max(...params.tasks.map((t: any) => t.estimatedSeconds!));
|
|
933
|
-
|
|
986
|
+
const parallelCwd = params.tasks[0]?.cwd ?? process.cwd();
|
|
987
|
+
const { limit: parallelLimit } = checkSyncLimit(0, parallelCwd);
|
|
988
|
+
if (syncPenalty > 0) {
|
|
989
|
+
const adjustedMax = maxParallelSeconds + syncPenalty;
|
|
990
|
+
if (adjustedMax >= parallelLimit) {
|
|
991
|
+
log.info("sync_penalty_rejected_parallel", "penalty", syncPenalty, "adjusted", adjustedMax);
|
|
992
|
+
return {
|
|
993
|
+
content: [{ type: "text" as const, text: `Rejected: your last sync agent '${lastSyncAgent}' estimated ${lastSyncEstimate}s but took ${lastSyncActual}s. Penalty: ${syncPenalty}s. Max parallel estimate ${maxParallelSeconds}s + penalty ${syncPenalty}s = ${adjustedMax}s — exceeds ${parallelLimit}s limit. Use async: true.` }],
|
|
994
|
+
details: mkd("parallel")([]),
|
|
995
|
+
isError: true,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (maxParallelSeconds >= parallelLimit && asyncOk) {
|
|
934
1000
|
return {
|
|
935
|
-
content: [{ type: "text" as const, text: SYNC_TIME_EXCEEDED_ERROR(maxParallelSeconds) }],
|
|
1001
|
+
content: [{ type: "text" as const, text: SYNC_TIME_EXCEEDED_ERROR(maxParallelSeconds, parallelLimit, parallelCwd) }],
|
|
936
1002
|
details: mkd("parallel")([]),
|
|
937
1003
|
isError: true,
|
|
938
1004
|
};
|
|
@@ -997,6 +1063,7 @@ export function registerSubagentTool(
|
|
|
997
1063
|
const label = t.name || t.agent;
|
|
998
1064
|
activeAgents.add(label);
|
|
999
1065
|
updateWorking();
|
|
1066
|
+
const taskExplicit = mergeModelOverride(t.model, explicit);
|
|
1000
1067
|
const r = await runSingleAgent(
|
|
1001
1068
|
agents,
|
|
1002
1069
|
t.agent,
|
|
@@ -1014,6 +1081,7 @@ export function registerSubagentTool(
|
|
|
1014
1081
|
parentModelId,
|
|
1015
1082
|
parentProvider,
|
|
1016
1083
|
params.persistSession,
|
|
1084
|
+
taskExplicit,
|
|
1017
1085
|
);
|
|
1018
1086
|
all[i] = r;
|
|
1019
1087
|
activeAgents.delete(t.name || t.agent);
|
|
@@ -1062,6 +1130,7 @@ export function registerSubagentTool(
|
|
|
1062
1130
|
parentProvider: string | undefined,
|
|
1063
1131
|
asyncOk: boolean,
|
|
1064
1132
|
sessionId?: string,
|
|
1133
|
+
explicit?: { model?: string; provider?: string },
|
|
1065
1134
|
) {
|
|
1066
1135
|
if (!params.cwd) {
|
|
1067
1136
|
return {
|
|
@@ -1089,9 +1158,21 @@ export function registerSubagentTool(
|
|
|
1089
1158
|
isError: true,
|
|
1090
1159
|
};
|
|
1091
1160
|
}
|
|
1092
|
-
|
|
1161
|
+
const { limit: singleLimit } = checkSyncLimit(0, params.cwd);
|
|
1162
|
+
if (syncPenalty > 0) {
|
|
1163
|
+
const adjustedEstimate = (params.estimatedSeconds ?? 0) + syncPenalty;
|
|
1164
|
+
if (adjustedEstimate >= singleLimit) {
|
|
1165
|
+
log.info("sync_penalty_rejected", params.agent, "estimate", params.estimatedSeconds, "penalty", syncPenalty, "adjusted", adjustedEstimate);
|
|
1166
|
+
return {
|
|
1167
|
+
content: [{ type: "text" as const, text: `Rejected: your last sync agent '${lastSyncAgent}' estimated ${lastSyncEstimate}s but took ${lastSyncActual}s. Penalty: ${syncPenalty}s. Your estimate ${params.estimatedSeconds}s + penalty ${syncPenalty}s = ${adjustedEstimate}s — exceeds ${singleLimit}s limit. Use async: true.` }],
|
|
1168
|
+
details: mkd("single")([]),
|
|
1169
|
+
isError: true,
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (params.estimatedSeconds >= singleLimit && asyncOk) {
|
|
1093
1174
|
return {
|
|
1094
|
-
content: [{ type: "text" as const, text: SYNC_TIME_EXCEEDED_ERROR(params.estimatedSeconds) }],
|
|
1175
|
+
content: [{ type: "text" as const, text: SYNC_TIME_EXCEEDED_ERROR(params.estimatedSeconds, singleLimit, params.cwd) }],
|
|
1095
1176
|
details: mkd("single")([]),
|
|
1096
1177
|
isError: true,
|
|
1097
1178
|
};
|
|
@@ -1104,6 +1185,8 @@ export function registerSubagentTool(
|
|
|
1104
1185
|
if (syncTaskId && syncTaskId !== "-1") {
|
|
1105
1186
|
await autoMarkInProgress(syncTaskId, params.cwd, sessionId).catch(() => {});
|
|
1106
1187
|
}
|
|
1188
|
+
log.info("sync_execute_start", params.agent, "taskId", syncTaskId ?? "none");
|
|
1189
|
+
const syncStartTime = Date.now();
|
|
1107
1190
|
const r = await runSingleAgent(
|
|
1108
1191
|
agents,
|
|
1109
1192
|
params.agent,
|
|
@@ -1116,7 +1199,17 @@ export function registerSubagentTool(
|
|
|
1116
1199
|
parentModelId,
|
|
1117
1200
|
parentProvider,
|
|
1118
1201
|
params.persistSession,
|
|
1202
|
+
explicit,
|
|
1119
1203
|
);
|
|
1204
|
+
const actualSeconds = Math.round((Date.now() - syncStartTime) / 1000);
|
|
1205
|
+
if (actualSeconds > checkSyncLimit(0, params.cwd).limit && actualSeconds - (params.estimatedSeconds ?? 0) > 30) {
|
|
1206
|
+
syncPenalty = actualSeconds - (params.estimatedSeconds ?? 0);
|
|
1207
|
+
lastSyncAgent = params.agent;
|
|
1208
|
+
lastSyncEstimate = params.estimatedSeconds ?? 0;
|
|
1209
|
+
lastSyncActual = actualSeconds;
|
|
1210
|
+
log.info("sync_penalty_set", params.agent, "estimate", params.estimatedSeconds, "actual", actualSeconds, "penalty", syncPenalty);
|
|
1211
|
+
}
|
|
1212
|
+
log.info("sync_execute_done", params.agent, "exitCode", r.exitCode, "stopReason", r.stopReason, "elapsed", Date.now() - syncStartTime);
|
|
1120
1213
|
activeAgents.delete(label);
|
|
1121
1214
|
updateWorking();
|
|
1122
1215
|
const err =
|
|
@@ -1138,6 +1231,7 @@ export function registerSubagentTool(
|
|
|
1138
1231
|
if (syncTaskId && syncTaskId !== "-1") {
|
|
1139
1232
|
await autoCompleteTask(syncTaskId, params.cwd, sessionId).catch(() => {});
|
|
1140
1233
|
}
|
|
1234
|
+
log.info("sync_execute_returning", params.agent);
|
|
1141
1235
|
return {
|
|
1142
1236
|
content: [
|
|
1143
1237
|
{ type: "text" as const, text: getFinalOutput(r.messages) || "(no output)" },
|
|
@@ -1451,6 +1545,8 @@ export function registerSubagentTool(
|
|
|
1451
1545
|
const confirm = params.confirmProjectAgents ?? true;
|
|
1452
1546
|
let parentModelId = ctx.model?.id as string | undefined;
|
|
1453
1547
|
let parentProvider = ctx.model?.provider as string | undefined;
|
|
1548
|
+
// Parse explicit model override from params (applies to all execution modes)
|
|
1549
|
+
const explicitOverride = parseModelOverride(params.model);
|
|
1454
1550
|
const asyncOk = supportsAsyncLlm(parentProvider, ctx.cwd);
|
|
1455
1551
|
let capabilityNote = "";
|
|
1456
1552
|
|
|
@@ -1577,9 +1673,11 @@ export function registerSubagentTool(
|
|
|
1577
1673
|
}
|
|
1578
1674
|
}
|
|
1579
1675
|
|
|
1676
|
+
const explicit = explicitOverride;
|
|
1677
|
+
|
|
1580
1678
|
// Async mode — spawn in background and return immediately
|
|
1581
1679
|
if (params.async === true) {
|
|
1582
|
-
const result = executeAsync(params, agents, mkd, parentModelId, parentProvider, ctx);
|
|
1680
|
+
const result = executeAsync(params, agents, mkd, parentModelId, parentProvider, ctx, explicit);
|
|
1583
1681
|
if (capabilityNote && result.content?.[0]?.type === "text") {
|
|
1584
1682
|
result.content[0].text = `${capabilityNote}\n\n${result.content[0].text}`;
|
|
1585
1683
|
}
|
|
@@ -1600,21 +1698,21 @@ export function registerSubagentTool(
|
|
|
1600
1698
|
// Chain mode
|
|
1601
1699
|
if (params.chain && params.chain.length > 0) {
|
|
1602
1700
|
return withCapabilityNote(
|
|
1603
|
-
await executeChain(params, agents, mkd, signal, onUpdate, activeAgents, updateWorking, parentModelId, parentProvider, asyncOk, sessionId),
|
|
1701
|
+
await executeChain(params, agents, mkd, signal, onUpdate, activeAgents, updateWorking, parentModelId, parentProvider, asyncOk, sessionId, explicit),
|
|
1604
1702
|
);
|
|
1605
1703
|
}
|
|
1606
1704
|
|
|
1607
1705
|
// Parallel mode
|
|
1608
1706
|
if (params.tasks && params.tasks.length > 0) {
|
|
1609
1707
|
return withCapabilityNote(
|
|
1610
|
-
await executeParallel(params, agents, mkd, signal, onUpdate, activeAgents, updateWorking, parentModelId, parentProvider, asyncOk, sessionId),
|
|
1708
|
+
await executeParallel(params, agents, mkd, signal, onUpdate, activeAgents, updateWorking, parentModelId, parentProvider, asyncOk, sessionId, explicit),
|
|
1611
1709
|
);
|
|
1612
1710
|
}
|
|
1613
1711
|
|
|
1614
1712
|
// Single mode
|
|
1615
1713
|
if (params.agent && params.task) {
|
|
1616
1714
|
return withCapabilityNote(
|
|
1617
|
-
await executeSingle(params, agents, mkd, signal, onUpdate, activeAgents, updateWorking, parentModelId, parentProvider, asyncOk, sessionId),
|
|
1715
|
+
await executeSingle(params, agents, mkd, signal, onUpdate, activeAgents, updateWorking, parentModelId, parentProvider, asyncOk, sessionId, explicit),
|
|
1618
1716
|
);
|
|
1619
1717
|
}
|
|
1620
1718
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync agent time-limit helpers.
|
|
3
|
+
* Kept separate from subagent-tool so unit tests can import without heavy deps.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getSetting } from "./project-settings.js";
|
|
7
|
+
import { createLogger } from "../shared/logger.js";
|
|
8
|
+
|
|
9
|
+
const log = createLogger("subagent");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if estimated seconds exceed the sync limit for a given cwd.
|
|
13
|
+
* Exported for testing.
|
|
14
|
+
*/
|
|
15
|
+
export function checkSyncLimit(
|
|
16
|
+
estimatedSeconds: number,
|
|
17
|
+
cwd: string,
|
|
18
|
+
): { exceeded: boolean; limit: number } {
|
|
19
|
+
const raw = getSetting(cwd, "sync_agent_max_seconds");
|
|
20
|
+
const limit = typeof raw === "number" && raw > 0 ? raw : 60; // fallback to default
|
|
21
|
+
log.debug("checkSyncLimit", "estimated", estimatedSeconds, "limit", limit, "exceeded", estimatedSeconds >= limit);
|
|
22
|
+
return { exceeded: estimatedSeconds >= limit, limit };
|
|
23
|
+
}
|
|
@@ -1,88 +1,98 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Task lifecycle helpers — auto-complete / mark in_progress via
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Task lifecycle helpers — auto-complete / mark in_progress via owned taskStore.
|
|
3
|
+
* Uses the exported store instance from extensions/pitasks when available.
|
|
4
|
+
* Falls back to _storeFactory for unit testing.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import * as os from "node:os";
|
|
8
7
|
import * as path from "node:path";
|
|
9
|
-
import {
|
|
8
|
+
import { createLogger } from "../shared/logger.js";
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
const taskStoreReady: Promise<void> = (async () => {
|
|
13
|
-
const candidates = [
|
|
14
|
-
"@tintinweb/pi-tasks/dist/task-store.js",
|
|
15
|
-
pathToFileURL(path.join(os.homedir(), ".pi/agent/npm/node_modules/@tintinweb/pi-tasks/dist/task-store.js")).href,
|
|
16
|
-
];
|
|
17
|
-
for (const candidate of candidates) {
|
|
18
|
-
try {
|
|
19
|
-
const mod = await import(candidate);
|
|
20
|
-
if (mod.TaskStore) { TaskStoreClass = mod.TaskStore; break; }
|
|
21
|
-
} catch { continue; }
|
|
22
|
-
}
|
|
23
|
-
if (!TaskStoreClass) {
|
|
24
|
-
throw new Error("[task-lifecycle] FATAL: TaskStore not found — @tintinweb/pi-tasks is required but failed to load");
|
|
25
|
-
}
|
|
26
|
-
})();
|
|
10
|
+
const log = createLogger("task-lifecycle");
|
|
27
11
|
|
|
28
|
-
/**
|
|
12
|
+
/** Get the shared task store instance (lazy import to avoid circular deps). */
|
|
13
|
+
async function getStore(): Promise<any> {
|
|
14
|
+
// Don't cache — pitasks reassigns taskStore during session upgrade/switch.
|
|
15
|
+
// Always read the latest export.
|
|
16
|
+
try {
|
|
17
|
+
const mod = await import("../pitasks/index.js");
|
|
18
|
+
return mod.taskStore ?? null;
|
|
19
|
+
} catch { return null; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Auto-complete a task (in-process, no AI involvement). */
|
|
29
23
|
export async function autoCompleteTask(taskId: string, cwd: string, sessionId?: string, _storeFactory?: (path: string) => any): Promise<boolean> {
|
|
30
24
|
if (!taskId || taskId === "-1") return false;
|
|
31
|
-
await taskStoreReady;
|
|
32
|
-
|
|
33
|
-
const tasksDir = path.join(cwd, ".pi", "tasks");
|
|
34
|
-
const candidates: string[] = [];
|
|
35
|
-
if (sessionId) candidates.push(path.join(tasksDir, `tasks-${sessionId}.json`));
|
|
36
|
-
candidates.push(path.join(tasksDir, "tasks.json"));
|
|
37
25
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
26
|
+
// TEST-ONLY: _storeFactory constructs file paths for unit test isolation.
|
|
27
|
+
// Production code NEVER passes this — it falls through to getStore() below.
|
|
28
|
+
if (_storeFactory) {
|
|
29
|
+
const tasksDir = path.join(cwd, ".pi", "tasks");
|
|
30
|
+
const candidates: string[] = [];
|
|
31
|
+
if (sessionId) candidates.push(path.join(tasksDir, `tasks-${sessionId}.json`));
|
|
32
|
+
candidates.push(path.join(tasksDir, "tasks.json"));
|
|
33
|
+
for (const storePath of candidates) {
|
|
34
|
+
let store: any;
|
|
35
|
+
try {
|
|
36
|
+
store = _storeFactory(storePath);
|
|
37
|
+
const task = store.get(taskId);
|
|
38
|
+
if (task) {
|
|
39
|
+
if (task.status !== "completed") { store.update(taskId, { status: "completed" }); return true; }
|
|
40
|
+
return false;
|
|
48
41
|
}
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
} catch (e: any) {
|
|
52
|
-
console.debug(`[task-lifecycle] autoCompleteTask failed for task ${taskId}: ${e?.message?.slice(0, 100)}`);
|
|
53
|
-
continue;
|
|
42
|
+
} catch { continue; }
|
|
43
|
+
finally { if (store && typeof store.close === "function") store.close(); }
|
|
54
44
|
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const store = await getStore();
|
|
49
|
+
if (!store) return false;
|
|
50
|
+
try {
|
|
51
|
+
const task = store.get(taskId);
|
|
52
|
+
if (!task) return false;
|
|
53
|
+
if (task.status !== "completed") { store.update(taskId, { status: "completed" }); return true; }
|
|
54
|
+
return false;
|
|
55
|
+
} catch (e: any) {
|
|
56
|
+
log.debug("autoCompleteTask failed for task", taskId, e?.message?.slice(0, 100));
|
|
57
|
+
return false;
|
|
55
58
|
}
|
|
56
|
-
return false;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
/** Auto-mark a task in_progress
|
|
61
|
+
/** Auto-mark a task in_progress (in-process, no AI involvement). */
|
|
60
62
|
export async function autoMarkInProgress(taskId: string, cwd: string, sessionId?: string, _storeFactory?: (path: string) => any): Promise<boolean> {
|
|
61
63
|
if (!taskId || taskId === "-1") return false;
|
|
62
|
-
await taskStoreReady;
|
|
63
|
-
|
|
64
|
-
const tasksDir = path.join(cwd, ".pi", "tasks");
|
|
65
|
-
const candidates: string[] = [];
|
|
66
|
-
if (sessionId) candidates.push(path.join(tasksDir, `tasks-${sessionId}.json`));
|
|
67
|
-
candidates.push(path.join(tasksDir, "tasks.json"));
|
|
68
64
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
65
|
+
// TEST-ONLY: _storeFactory constructs file paths for unit test isolation.
|
|
66
|
+
// Production code NEVER passes this — it falls through to getStore() below.
|
|
67
|
+
if (_storeFactory) {
|
|
68
|
+
const tasksDir = path.join(cwd, ".pi", "tasks");
|
|
69
|
+
const candidates: string[] = [];
|
|
70
|
+
if (sessionId) candidates.push(path.join(tasksDir, `tasks-${sessionId}.json`));
|
|
71
|
+
candidates.push(path.join(tasksDir, "tasks.json"));
|
|
72
|
+
for (const storePath of candidates) {
|
|
73
|
+
let store: any;
|
|
74
|
+
try {
|
|
75
|
+
store = _storeFactory(storePath);
|
|
76
|
+
const task = store.get(taskId);
|
|
77
|
+
if (task) {
|
|
78
|
+
if (task.status === "pending") { store.update(taskId, { status: "in_progress" }); return true; }
|
|
79
|
+
return false;
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
} catch (e: any) {
|
|
83
|
-
console.debug(`[task-lifecycle] autoMarkInProgress failed for task ${taskId}: ${e?.message?.slice(0, 100)}`);
|
|
84
|
-
continue;
|
|
81
|
+
} catch { continue; }
|
|
82
|
+
finally { if (store && typeof store.close === "function") store.close(); }
|
|
85
83
|
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const store = await getStore();
|
|
88
|
+
if (!store) return false;
|
|
89
|
+
try {
|
|
90
|
+
const task = store.get(taskId);
|
|
91
|
+
if (!task) return false;
|
|
92
|
+
if (task.status === "pending") { store.update(taskId, { status: "in_progress" }); return true; }
|
|
93
|
+
return false;
|
|
94
|
+
} catch (e: any) {
|
|
95
|
+
log.debug("autoMarkInProgress failed for task", taskId, e?.message?.slice(0, 100));
|
|
96
|
+
return false;
|
|
86
97
|
}
|
|
87
|
-
return false;
|
|
88
98
|
}
|