@agentproto/runtime 0.3.0 → 0.4.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.
@@ -62,5 +62,70 @@ declare const RESUME_STRATEGIES: Record<string, ResumeStrategy>;
62
62
  * applicable.
63
63
  */
64
64
  declare function hasResumeStrategy(adapterSlug: string | undefined): boolean;
65
+ interface RestartCandidate {
66
+ adapterSlug?: string;
67
+ resumeMetadata?: Record<string, string>;
68
+ pty?: boolean;
69
+ adapterSessionId?: string;
70
+ }
71
+ interface FsProbeCandidate extends RestartCandidate {
72
+ cwd?: string;
73
+ startedAt: string;
74
+ }
75
+ /**
76
+ * Where a restart should land. Computed once so callers never diverge:
77
+ *
78
+ * 1. pty-native — the adapter has a captured resume id AND declares
79
+ * `spawnArgs` (e.g. claude-code): respawn a PTY running the
80
+ * provider's own resume command. Most reliable — works whenever
81
+ * the provider persisted the session, regardless of whether the
82
+ * ACP wrapper did.
83
+ * 2. pty-plain — the previous session was a real PTY with no
84
+ * native strategy match: re-run the same argv, no continuity.
85
+ * 3. agent — an agent-cli session with no native strategy:
86
+ * resume at the ACP level via the adapter's own session id (may
87
+ * still 404 if the adapter never persisted a turn — callers
88
+ * should retry without `resumeSessionId` on a "not found" error).
89
+ * 4. unsupported — a generic command session; no history to resume
90
+ * and no shape to clone reliably.
91
+ */
92
+ type RestartStrategy = {
93
+ kind: "pty-native";
94
+ argv: string[];
95
+ } | {
96
+ kind: "pty-plain";
97
+ } | {
98
+ kind: "agent";
99
+ resumeSessionId?: string;
100
+ } | {
101
+ kind: "unsupported";
102
+ reason: string;
103
+ };
104
+ declare function decideRestartStrategy(prev: RestartCandidate): RestartStrategy;
105
+ /**
106
+ * Generic filesystem-fallback: for each known adapter strategy that
107
+ * defines an `fsProbe`, run it against this session's cwd. If a
108
+ * resume id is found, attach it to the descriptor's resumeMetadata.
109
+ *
110
+ * Lets `restart` recover continuity even when our own output sniffer
111
+ * missed the resume hint (session killed too quickly, output buffered
112
+ * past the kill, …). Eligible only when the file is at-or-after the
113
+ * session's `startedAt` (avoid resuming an unrelated prior session
114
+ * in the same cwd — see ResumeStrategy.fsProbe comment).
115
+ */
116
+ declare function augmentWithFsResume<T extends FsProbeCandidate>(prev: T): Promise<T>;
117
+ /**
118
+ * Describe which resume path a restart used, for CLI banners / MCP
119
+ * responses. Returns a short phrase like "resumed via claude --resume",
120
+ * or "resumed via ACP", or "" when no resume was attempted.
121
+ */
122
+ declare function describeResumePath(prev: RestartCandidate): string;
123
+ /**
124
+ * Shell-style argv tokenizer for descriptors that don't carry `argv`
125
+ * separately (legacy persisted rows predating that field). Same rules
126
+ * as the spawn-dialog tokenizer in the web app: whitespace splits,
127
+ * single + double quotes group.
128
+ */
129
+ declare function tokenizeCommand(s: string): string[];
65
130
 
66
- export { RESUME_STRATEGIES, type ResumeMetadataKey, type ResumeStrategy, hasResumeStrategy };
131
+ export { type FsProbeCandidate, RESUME_STRATEGIES, type RestartCandidate, type RestartStrategy, type ResumeMetadataKey, type ResumeStrategy, augmentWithFsResume, decideRestartStrategy, describeResumePath, hasResumeStrategy, tokenizeCommand };
@@ -58,7 +58,86 @@ function hasResumeStrategy(adapterSlug) {
58
58
  const s = RESUME_STRATEGIES[adapterSlug];
59
59
  return !!(s && (s.outputHint || s.fsProbe || s.spawnArgs));
60
60
  }
61
+ function decideRestartStrategy(prev) {
62
+ if (prev.adapterSlug) {
63
+ const strategy = RESUME_STRATEGIES[prev.adapterSlug];
64
+ const id = strategy?.storeAs ? prev.resumeMetadata?.[strategy.storeAs] : void 0;
65
+ if (strategy?.spawnArgs && id) {
66
+ return { kind: "pty-native", argv: strategy.spawnArgs(id) };
67
+ }
68
+ }
69
+ if (prev.pty === true) {
70
+ return { kind: "pty-plain" };
71
+ }
72
+ if (prev.adapterSlug) {
73
+ return {
74
+ kind: "agent",
75
+ ...prev.adapterSessionId ? { resumeSessionId: prev.adapterSessionId } : {}
76
+ };
77
+ }
78
+ return {
79
+ kind: "unsupported",
80
+ reason: "generic command session \u2014 restart only supports pty + agent-cli"
81
+ };
82
+ }
83
+ async function augmentWithFsResume(prev) {
84
+ const slug = prev.adapterSlug;
85
+ if (!slug) return prev;
86
+ const strategy = RESUME_STRATEGIES[slug];
87
+ if (!strategy?.fsProbe) return prev;
88
+ if (prev.resumeMetadata?.[strategy.storeAs]) return prev;
89
+ if (!prev.cwd) return prev;
90
+ const id = await strategy.fsProbe(prev.cwd, prev.startedAt);
91
+ if (!id) return prev;
92
+ return {
93
+ ...prev,
94
+ resumeMetadata: {
95
+ ...prev.resumeMetadata ?? {},
96
+ [strategy.storeAs]: id
97
+ }
98
+ };
99
+ }
100
+ function describeResumePath(prev) {
101
+ if (prev.adapterSlug) {
102
+ const s = RESUME_STRATEGIES[prev.adapterSlug];
103
+ if (s?.spawnArgs && s.storeAs && prev.resumeMetadata?.[s.storeAs]) {
104
+ const sample = s.spawnArgs("\u2026")[0] ?? prev.adapterSlug;
105
+ return `resumed via ${sample} --resume`;
106
+ }
107
+ }
108
+ if (prev.adapterSlug && prev.adapterSessionId) {
109
+ return "resumed via ACP";
110
+ }
111
+ return "";
112
+ }
113
+ function tokenizeCommand(s) {
114
+ const out = [];
115
+ let buf = "";
116
+ let inSingle = false;
117
+ let inDouble = false;
118
+ for (let i = 0; i < s.length; i++) {
119
+ const ch = s[i];
120
+ if (ch === "'" && !inDouble) {
121
+ inSingle = !inSingle;
122
+ continue;
123
+ }
124
+ if (ch === '"' && !inSingle) {
125
+ inDouble = !inDouble;
126
+ continue;
127
+ }
128
+ if (!inSingle && !inDouble && /\s/.test(ch ?? "")) {
129
+ if (buf) {
130
+ out.push(buf);
131
+ buf = "";
132
+ }
133
+ continue;
134
+ }
135
+ buf += ch;
136
+ }
137
+ if (buf) out.push(buf);
138
+ return out;
139
+ }
61
140
 
62
- export { RESUME_STRATEGIES, hasResumeStrategy };
141
+ export { RESUME_STRATEGIES, augmentWithFsResume, decideRestartStrategy, describeResumePath, hasResumeStrategy, tokenizeCommand };
63
142
  //# sourceMappingURL=resume-strategies.mjs.map
64
143
  //# sourceMappingURL=resume-strategies.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/resume-strategies.ts"],"names":["fs"],"mappings":";;;;;;;;;AAoEO,IAAM,iBAAA,GAAoD;AAAA,EAC/D,aAAA,EAAe;AAAA;AAAA;AAAA,IAGb,UAAA,EAAY,sCAAA;AAAA,IACZ,OAAA,EAAS,gBAAA;AAAA,IACT,OAAA,EAAS,sBAAsB,kBAAkB,CAAA;AAAA,IACjD,SAAA,EAAW,CAAA,EAAA,KAAM,CAAC,QAAA,EAAU,YAAY,EAAE;AAAA;AAC5C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUF;AASA,SAAS,sBACP,QAAA,EACgE;AAChE,EAAA,OAAO,OAAO,KAAK,aAAA,KAAkB;AACnC,IAAA,MAAM,OAAA,GAAU,GAAA,CAAI,OAAA,CAAQ,KAAA,EAAO,GAAG,CAAA;AACtC,IAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,OAAA,EAAQ,EAAG,UAAU,OAAO,CAAA;AAChD,IAAA,IAAI,OAAA;AACJ,IAAA,IAAI;AACF,MAAA,OAAA,GAAU,MAAMA,QAAA,CAAG,OAAA,CAAQ,GAAG,CAAA;AAAA,IAChC,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,IAAA;AAAA,IACT;AACA,IAAA,MAAM,QAAQ,OAAA,CAAQ,MAAA,CAAO,OAAK,CAAA,CAAE,QAAA,CAAS,QAAQ,CAAC,CAAA;AACtD,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AAC/B,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,KAAA,CAAM,aAAa,CAAA;AAC5C,IAAA,MAAM,aAAgD,EAAC;AACvD,IAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,MAAA,IAAI;AACF,QAAA,MAAM,KAAK,MAAMA,QAAA,CAAG,KAAK,IAAA,CAAK,GAAA,EAAK,CAAC,CAAC,CAAA;AACrC,QAAA,IAAI,CAAC,OAAO,QAAA,CAAS,WAAW,KAAK,EAAA,CAAG,OAAA,IAAW,cAAc,GAAA,EAAM;AACrE,UAAA,UAAA,CAAW,KAAK,EAAE,IAAA,EAAM,GAAG,KAAA,EAAO,EAAA,CAAG,SAAS,CAAA;AAAA,QAChD;AAAA,MACF,CAAA,CAAA,MAAQ;AAAA,MAER;AAAA,IACF;AACA,IAAA,IAAI,UAAA,CAAW,MAAA,KAAW,CAAA,EAAG,OAAO,IAAA;AACpC,IAAA,UAAA,CAAW,KAAK,CAAC,CAAA,EAAG,MAAM,CAAA,CAAE,KAAA,GAAQ,EAAE,KAAK,CAAA;AAC3C,IAAA,OAAO,WAAW,CAAC,CAAA,CAAG,IAAA,CAAK,OAAA,CAAQ,YAAY,EAAE,CAAA;AAAA,EACnD,CAAA;AACF;AAQO,SAAS,kBAAkB,WAAA,EAA0C;AAC1E,EAAA,IAAI,CAAC,aAAa,OAAO,KAAA;AACzB,EAAA,MAAM,CAAA,GAAI,kBAAkB,WAAW,CAAA;AACvC,EAAA,OAAO,CAAC,EAAE,CAAA,KAAM,EAAE,UAAA,IAAc,CAAA,CAAE,WAAW,CAAA,CAAE,SAAA,CAAA,CAAA;AACjD","file":"resume-strategies.mjs","sourcesContent":["/**\n * Per-adapter resume strategies — the daemon's view of \"how do I\n * continue a previous session for THIS adapter?\".\n *\n * The driver already has an `AgentCliContinuation` declaration in\n * the adapter manifest (AIP-45). That covers strategy NAMES\n * (`native-resume`, `pinned-session`, `transcript`, `none`) but is\n * silent on the mechanics that live OUTSIDE the runtime — like\n * \"where on disk does this adapter store its conversations?\" or\n * \"what argv do I pass to resume into PTY mode?\".\n *\n * This table is that practical bridge. Each entry declares three\n * optional hooks:\n *\n * outputHint regex matched against each output line; group 1 is\n * the resume id. Stored as `desc.resumeMetadata[storeAs]`.\n * fsProbe async function that probes a per-adapter directory\n * for a stored session id, scoped to a workspace cwd.\n * Used when `outputHint` never matched.\n * spawnArgs given a captured id, returns the argv to spawn a\n * PTY that resumes the session. agentproto then\n * bypasses ACP/agent-cli and runs the provider\n * directly — the most reliable resume path because\n * it leans on the provider's native UX.\n *\n * Adapters that don't declare anything here fall back to:\n * 1. ACP-level resume via `resumeSessionId` on POST /sessions/agent\n * 2. Fresh spawn (same shape, no continuity)\n *\n * Eventually each adapter should ship its own strategy on\n * `AgentCliHandle.resumeStrategy` (driver-side). This central table\n * is the bootstrap: it lets a new adapter become resume-capable\n * without bumping its npm package; once that lands, entries\n * graduate into the adapter packages.\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\n\n/** Where on disk the resume id ends up on the session descriptor. */\nexport type ResumeMetadataKey =\n | \"claudeResumeId\"\n | \"hermesResumeId\"\n | \"codexResumeId\"\n | \"openClawResumeId\"\n | \"openCodeResumeId\"\n\nexport interface ResumeStrategy {\n /** When set, run on every output line for this adapter. Group 1\n * captures the resume id. The match is saved on the descriptor\n * as `resumeMetadata[storeAs]`. */\n outputHint?: RegExp\n /** Where the matched id is recorded on the descriptor. */\n storeAs: ResumeMetadataKey\n /** Probe the adapter's on-disk session store for the most-recently\n * modified session in the given workspace. Returns the resume id\n * or null when nothing eligible. Files older than `prevStartedAt`\n * are ignored (avoids resuming an unrelated prior conversation).\n *\n * Skip when the adapter doesn't persist sessions externally. */\n fsProbe?(cwd: string, prevStartedAt: string): Promise<string | null>\n /** Return the argv to spawn a PTY that resumes into the given id.\n * When omitted, the daemon falls back to ACP-level resume via\n * the agent-cli protocol instead of the provider's native CLI. */\n spawnArgs?(id: string): string[]\n}\n\nexport const RESUME_STRATEGIES: Record<string, ResumeStrategy> = {\n \"claude-code\": {\n // Printed by claude on graceful exit when session persistence\n // is on (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`\n outputHint: /claude\\s+--resume\\s+([0-9a-f-]{8,})/i,\n storeAs: \"claudeResumeId\",\n fsProbe: probeMtimeLatestJsonl(\".claude/projects\"),\n spawnArgs: id => [\"claude\", \"--resume\", id],\n },\n\n // Stubs for other shipped adapters — fill in as we learn each\n // provider's resume mechanism. Today they fall back to ACP-level\n // resume (whatever the agent-cli runtime supports) or fresh spawn.\n //\n // hermes: { storeAs: \"hermesResumeId\", ... }\n // codex: { storeAs: \"codexResumeId\", ... }\n // openclaw: { storeAs: \"openClawResumeId\", ... }\n // opencode: { storeAs: \"openCodeResumeId\", ... }\n}\n\n/**\n * Build an `fsProbe` for adapters that store one `.jsonl` per session\n * inside `~/<storeRel>/<cwd-encoded>/`. The encoded cwd is the\n * absolute path with `/` → `-` (claude's convention; others tend to\n * use the same scheme). Returns the UUID of the most-recently\n * modified `.jsonl`, filtered to files at-or-after `prevStartedAt`.\n */\nfunction probeMtimeLatestJsonl(\n storeRel: string,\n): (cwd: string, prevStartedAt: string) => Promise<string | null> {\n return async (cwd, prevStartedAt) => {\n const encoded = cwd.replace(/\\//g, \"-\")\n const dir = resolve(homedir(), storeRel, encoded)\n let entries: string[]\n try {\n entries = await fs.readdir(dir)\n } catch {\n return null\n }\n const jsonl = entries.filter(e => e.endsWith(\".jsonl\"))\n if (jsonl.length === 0) return null\n const startedAtMs = Date.parse(prevStartedAt)\n const candidates: { name: string; mtime: number }[] = []\n for (const f of jsonl) {\n try {\n const st = await fs.stat(join(dir, f))\n if (!Number.isFinite(startedAtMs) || st.mtimeMs >= startedAtMs - 1000) {\n candidates.push({ name: f, mtime: st.mtimeMs })\n }\n } catch {\n // ignore unreadable entry\n }\n }\n if (candidates.length === 0) return null\n candidates.sort((a, b) => b.mtime - a.mtime)\n return candidates[0]!.name.replace(/\\.jsonl$/, \"\")\n }\n}\n\n/**\n * Helper: which adapters declare any resume capability? Used by\n * `agentproto sessions restart` to print a helpful \"this adapter\n * doesn't support resume; falling back to fresh spawn\" hint when\n * applicable.\n */\nexport function hasResumeStrategy(adapterSlug: string | undefined): boolean {\n if (!adapterSlug) return false\n const s = RESUME_STRATEGIES[adapterSlug]\n return !!(s && (s.outputHint || s.fsProbe || s.spawnArgs))\n}\n"]}
1
+ {"version":3,"sources":["../src/resume-strategies.ts"],"names":["fs"],"mappings":"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","file":"resume-strategies.mjs","sourcesContent":["/**\n * Per-adapter resume strategies — the daemon's view of \"how do I\n * continue a previous session for THIS adapter?\".\n *\n * The driver already has an `AgentCliContinuation` declaration in\n * the adapter manifest (AIP-45). That covers strategy NAMES\n * (`native-resume`, `pinned-session`, `transcript`, `none`) but is\n * silent on the mechanics that live OUTSIDE the runtime — like\n * \"where on disk does this adapter store its conversations?\" or\n * \"what argv do I pass to resume into PTY mode?\".\n *\n * This table is that practical bridge. Each entry declares three\n * optional hooks:\n *\n * outputHint regex matched against each output line; group 1 is\n * the resume id. Stored as `desc.resumeMetadata[storeAs]`.\n * fsProbe async function that probes a per-adapter directory\n * for a stored session id, scoped to a workspace cwd.\n * Used when `outputHint` never matched.\n * spawnArgs given a captured id, returns the argv to spawn a\n * PTY that resumes the session. agentproto then\n * bypasses ACP/agent-cli and runs the provider\n * directly — the most reliable resume path because\n * it leans on the provider's native UX.\n *\n * Adapters that don't declare anything here fall back to:\n * 1. ACP-level resume via `resumeSessionId` on POST /sessions/agent\n * 2. Fresh spawn (same shape, no continuity)\n *\n * Eventually each adapter should ship its own strategy on\n * `AgentCliHandle.resumeStrategy` (driver-side). This central table\n * is the bootstrap: it lets a new adapter become resume-capable\n * without bumping its npm package; once that lands, entries\n * graduate into the adapter packages.\n */\n\nimport { promises as fs } from \"node:fs\"\nimport { homedir } from \"node:os\"\nimport { join, resolve } from \"node:path\"\n\n/** Where on disk the resume id ends up on the session descriptor. */\nexport type ResumeMetadataKey =\n | \"claudeResumeId\"\n | \"hermesResumeId\"\n | \"codexResumeId\"\n | \"openClawResumeId\"\n | \"openCodeResumeId\"\n\nexport interface ResumeStrategy {\n /** When set, run on every output line for this adapter. Group 1\n * captures the resume id. The match is saved on the descriptor\n * as `resumeMetadata[storeAs]`. */\n outputHint?: RegExp\n /** Where the matched id is recorded on the descriptor. */\n storeAs: ResumeMetadataKey\n /** Probe the adapter's on-disk session store for the most-recently\n * modified session in the given workspace. Returns the resume id\n * or null when nothing eligible. Files older than `prevStartedAt`\n * are ignored (avoids resuming an unrelated prior conversation).\n *\n * Skip when the adapter doesn't persist sessions externally. */\n fsProbe?(cwd: string, prevStartedAt: string): Promise<string | null>\n /** Return the argv to spawn a PTY that resumes into the given id.\n * When omitted, the daemon falls back to ACP-level resume via\n * the agent-cli protocol instead of the provider's native CLI. */\n spawnArgs?(id: string): string[]\n}\n\nexport const RESUME_STRATEGIES: Record<string, ResumeStrategy> = {\n \"claude-code\": {\n // Printed by claude on graceful exit when session persistence\n // is on (default). Example: `claude --resume 0e483f81-1a44-4bec-9667-b37158450296`\n outputHint: /claude\\s+--resume\\s+([0-9a-f-]{8,})/i,\n storeAs: \"claudeResumeId\",\n fsProbe: probeMtimeLatestJsonl(\".claude/projects\"),\n spawnArgs: id => [\"claude\", \"--resume\", id],\n },\n\n // Stubs for other shipped adapters — fill in as we learn each\n // provider's resume mechanism. Today they fall back to ACP-level\n // resume (whatever the agent-cli runtime supports) or fresh spawn.\n //\n // hermes: { storeAs: \"hermesResumeId\", ... }\n // codex: { storeAs: \"codexResumeId\", ... }\n // openclaw: { storeAs: \"openClawResumeId\", ... }\n // opencode: { storeAs: \"openCodeResumeId\", ... }\n}\n\n/**\n * Build an `fsProbe` for adapters that store one `.jsonl` per session\n * inside `~/<storeRel>/<cwd-encoded>/`. The encoded cwd is the\n * absolute path with `/` → `-` (claude's convention; others tend to\n * use the same scheme). Returns the UUID of the most-recently\n * modified `.jsonl`, filtered to files at-or-after `prevStartedAt`.\n */\nfunction probeMtimeLatestJsonl(\n storeRel: string,\n): (cwd: string, prevStartedAt: string) => Promise<string | null> {\n return async (cwd, prevStartedAt) => {\n const encoded = cwd.replace(/\\//g, \"-\")\n const dir = resolve(homedir(), storeRel, encoded)\n let entries: string[]\n try {\n entries = await fs.readdir(dir)\n } catch {\n return null\n }\n const jsonl = entries.filter(e => e.endsWith(\".jsonl\"))\n if (jsonl.length === 0) return null\n const startedAtMs = Date.parse(prevStartedAt)\n const candidates: { name: string; mtime: number }[] = []\n for (const f of jsonl) {\n try {\n const st = await fs.stat(join(dir, f))\n if (!Number.isFinite(startedAtMs) || st.mtimeMs >= startedAtMs - 1000) {\n candidates.push({ name: f, mtime: st.mtimeMs })\n }\n } catch {\n // ignore unreadable entry\n }\n }\n if (candidates.length === 0) return null\n candidates.sort((a, b) => b.mtime - a.mtime)\n return candidates[0]!.name.replace(/\\.jsonl$/, \"\")\n }\n}\n\n/**\n * Helper: which adapters declare any resume capability? Used by\n * `agentproto sessions restart` to print a helpful \"this adapter\n * doesn't support resume; falling back to fresh spawn\" hint when\n * applicable.\n */\nexport function hasResumeStrategy(adapterSlug: string | undefined): boolean {\n if (!adapterSlug) return false\n const s = RESUME_STRATEGIES[adapterSlug]\n return !!(s && (s.outputHint || s.fsProbe || s.spawnArgs))\n}\n\n// ── Restart decision tree ────────────────────────────────────────\n//\n// Shared between the CLI (`agentproto sessions restart`, sessions.ts)\n// and the daemon's `session_restart` MCP tool (session-tools.ts) —\n// both need to answer \"given this dead/alive session, how do I bring\n// it back with the most continuity?\" the exact same way. Duck-typed\n// on a minimal shape rather than importing `SessionDescriptor` from\n// sessions.ts: this file is its own tsup entry (`splitting: false`),\n// so a value-level cross-entry import would inline the whole sessions\n// module here. A full `SessionDescriptor` satisfies these structurally.\n\nexport interface RestartCandidate {\n adapterSlug?: string\n resumeMetadata?: Record<string, string>\n pty?: boolean\n adapterSessionId?: string\n}\n\nexport interface FsProbeCandidate extends RestartCandidate {\n cwd?: string\n startedAt: string\n}\n\n/**\n * Where a restart should land. Computed once so callers never diverge:\n *\n * 1. pty-native — the adapter has a captured resume id AND declares\n * `spawnArgs` (e.g. claude-code): respawn a PTY running the\n * provider's own resume command. Most reliable — works whenever\n * the provider persisted the session, regardless of whether the\n * ACP wrapper did.\n * 2. pty-plain — the previous session was a real PTY with no\n * native strategy match: re-run the same argv, no continuity.\n * 3. agent — an agent-cli session with no native strategy:\n * resume at the ACP level via the adapter's own session id (may\n * still 404 if the adapter never persisted a turn — callers\n * should retry without `resumeSessionId` on a \"not found\" error).\n * 4. unsupported — a generic command session; no history to resume\n * and no shape to clone reliably.\n */\nexport type RestartStrategy =\n | { kind: \"pty-native\"; argv: string[] }\n | { kind: \"pty-plain\" }\n | { kind: \"agent\"; resumeSessionId?: string }\n | { kind: \"unsupported\"; reason: string }\n\nexport function decideRestartStrategy(prev: RestartCandidate): RestartStrategy {\n // Provider-native resume takes precedence over ACP-level resume —\n // strictly more reliable when available.\n if (prev.adapterSlug) {\n const strategy = RESUME_STRATEGIES[prev.adapterSlug]\n const id = strategy?.storeAs\n ? prev.resumeMetadata?.[strategy.storeAs]\n : undefined\n if (strategy?.spawnArgs && id) {\n return { kind: \"pty-native\", argv: strategy.spawnArgs(id) }\n }\n }\n if (prev.pty === true) {\n return { kind: \"pty-plain\" }\n }\n if (prev.adapterSlug) {\n return {\n kind: \"agent\",\n ...(prev.adapterSessionId\n ? { resumeSessionId: prev.adapterSessionId }\n : {}),\n }\n }\n return {\n kind: \"unsupported\",\n reason: \"generic command session — restart only supports pty + agent-cli\",\n }\n}\n\n/**\n * Generic filesystem-fallback: for each known adapter strategy that\n * defines an `fsProbe`, run it against this session's cwd. If a\n * resume id is found, attach it to the descriptor's resumeMetadata.\n *\n * Lets `restart` recover continuity even when our own output sniffer\n * missed the resume hint (session killed too quickly, output buffered\n * past the kill, …). Eligible only when the file is at-or-after the\n * session's `startedAt` (avoid resuming an unrelated prior session\n * in the same cwd — see ResumeStrategy.fsProbe comment).\n */\nexport async function augmentWithFsResume<T extends FsProbeCandidate>(\n prev: T,\n): Promise<T> {\n const slug = prev.adapterSlug\n if (!slug) return prev\n const strategy = RESUME_STRATEGIES[slug]\n if (!strategy?.fsProbe) return prev\n // If we already have a captured id for this strategy's storage key,\n // skip the FS probe — the sniffer found it during the session.\n if (prev.resumeMetadata?.[strategy.storeAs]) return prev\n if (!prev.cwd) return prev\n const id = await strategy.fsProbe(prev.cwd, prev.startedAt)\n if (!id) return prev\n return {\n ...prev,\n resumeMetadata: {\n ...(prev.resumeMetadata ?? {}),\n [strategy.storeAs]: id,\n },\n }\n}\n\n/**\n * Describe which resume path a restart used, for CLI banners / MCP\n * responses. Returns a short phrase like \"resumed via claude --resume\",\n * or \"resumed via ACP\", or \"\" when no resume was attempted.\n */\nexport function describeResumePath(prev: RestartCandidate): string {\n if (prev.adapterSlug) {\n const s = RESUME_STRATEGIES[prev.adapterSlug]\n if (s?.spawnArgs && s.storeAs && prev.resumeMetadata?.[s.storeAs]) {\n const sample = s.spawnArgs(\"…\")[0] ?? prev.adapterSlug\n return `resumed via ${sample} --resume`\n }\n }\n if (prev.adapterSlug && prev.adapterSessionId) {\n return \"resumed via ACP\"\n }\n return \"\"\n}\n\n/**\n * Shell-style argv tokenizer for descriptors that don't carry `argv`\n * separately (legacy persisted rows predating that field). Same rules\n * as the spawn-dialog tokenizer in the web app: whitespace splits,\n * single + double quotes group.\n */\nexport function tokenizeCommand(s: string): string[] {\n const out: string[] = []\n let buf = \"\"\n let inSingle = false\n let inDouble = false\n for (let i = 0; i < s.length; i++) {\n const ch = s[i]\n if (ch === \"'\" && !inDouble) {\n inSingle = !inSingle\n continue\n }\n if (ch === '\"' && !inSingle) {\n inDouble = !inDouble\n continue\n }\n if (!inSingle && !inDouble && /\\s/.test(ch ?? \"\")) {\n if (buf) {\n out.push(buf)\n buf = \"\"\n }\n continue\n }\n buf += ch\n }\n if (buf) out.push(buf)\n return out\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentproto/runtime",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "@agentproto/runtime — long-running gateway that turns an agentproto workspace into a live runtime. Composes @agentproto/mcp-server (CRUD verbs) with HTTP transport, HEARTBEAT.md autonomy loop, and append-only conversation persistence. Drop a workspace dir, point your MCP client at it, and the agent ticks on its own.",
5
5
  "keywords": [
6
6
  "agentproto",
@@ -81,15 +81,17 @@
81
81
  "access": "public"
82
82
  },
83
83
  "dependencies": {
84
+ "croner": "^9.0.0",
84
85
  "@modelcontextprotocol/sdk": "^1.29.0",
85
86
  "gray-matter": "^4.0.3",
86
87
  "ws": "^8.18.0",
87
88
  "zod": "^4.4.3",
88
- "@agentproto/acp": "0.2.0",
89
+ "@agentproto/acp": "0.3.0",
90
+ "@agentproto/workflow-runtime": "0.2.0",
89
91
  "@agentproto/adapter-kit": "0.1.0",
90
92
  "@agentproto/agent": "0.2.0",
91
- "@agentproto/mcp-server": "0.2.1",
92
- "@agentproto/manifest": "0.2.0"
93
+ "@agentproto/manifest": "0.2.0",
94
+ "@agentproto/mcp-server": "0.2.1"
93
95
  },
94
96
  "devDependencies": {
95
97
  "@types/node": "^25.6.2",