@skrr-ai/cli 0.1.86 → 0.1.88

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 (103) hide show
  1. package/bin/run.js +11 -0
  2. package/dist/base-command.js +1 -0
  3. package/dist/commands/agents/chat.d.ts +58 -1
  4. package/dist/commands/agents/chat.js +206 -4
  5. package/dist/commands/computer/index.d.ts +19 -0
  6. package/dist/commands/computer/index.js +37 -0
  7. package/dist/commands/labels/create.d.ts +1 -0
  8. package/dist/commands/labels/create.js +2 -0
  9. package/dist/commands/labels/restore.d.ts +1 -0
  10. package/dist/commands/labels/restore.js +6 -3
  11. package/dist/commands/labels/update.d.ts +1 -0
  12. package/dist/commands/labels/update.js +2 -0
  13. package/dist/commands/machines/computer-adoption.d.ts +15 -0
  14. package/dist/commands/machines/computer-adoption.js +33 -0
  15. package/dist/commands/machines/dedicated/archive.d.ts +26 -0
  16. package/dist/commands/machines/dedicated/archive.js +113 -0
  17. package/dist/commands/machines/dedicated/audit.d.ts +25 -0
  18. package/dist/commands/machines/dedicated/audit.js +113 -0
  19. package/dist/commands/machines/dedicated/computer.d.ts +23 -0
  20. package/dist/commands/machines/dedicated/computer.js +64 -0
  21. package/dist/commands/machines/dedicated/cp.d.ts +2 -0
  22. package/dist/commands/machines/dedicated/cp.js +74 -2
  23. package/dist/commands/machines/dedicated/git-status.d.ts +15 -0
  24. package/dist/commands/machines/dedicated/git-status.js +56 -0
  25. package/dist/commands/machines/dedicated/index.js +15 -1
  26. package/dist/commands/machines/dedicated/ls.d.ts +27 -0
  27. package/dist/commands/machines/dedicated/ls.js +100 -0
  28. package/dist/commands/machines/dedicated/mkdir.d.ts +18 -0
  29. package/dist/commands/machines/dedicated/mkdir.js +57 -0
  30. package/dist/commands/machines/dedicated/mv.d.ts +24 -0
  31. package/dist/commands/machines/dedicated/mv.js +83 -0
  32. package/dist/commands/machines/dedicated/rm.d.ts +26 -0
  33. package/dist/commands/machines/dedicated/rm.js +100 -0
  34. package/dist/commands/machines/dedicated/search.d.ts +24 -0
  35. package/dist/commands/machines/dedicated/search.js +78 -0
  36. package/dist/commands/machines/dedicated/stat.d.ts +22 -0
  37. package/dist/commands/machines/dedicated/stat.js +92 -0
  38. package/dist/commands/machines/hosted/index.js +1 -1
  39. package/dist/commands/machines/hosted/list.js +1 -1
  40. package/dist/commands/machines/hosted/start.js +1 -1
  41. package/dist/commands/machines/services/declare.d.ts +24 -0
  42. package/dist/commands/machines/services/declare.js +73 -0
  43. package/dist/commands/machines/services/index.d.ts +15 -0
  44. package/dist/commands/machines/services/index.js +31 -0
  45. package/dist/commands/machines/services/ls.d.ts +17 -0
  46. package/dist/commands/machines/services/ls.js +60 -0
  47. package/dist/commands/machines/services/withdraw.d.ts +19 -0
  48. package/dist/commands/machines/services/withdraw.js +47 -0
  49. package/dist/commands/machines/share.d.ts +29 -0
  50. package/dist/commands/machines/share.js +100 -0
  51. package/dist/commands/machines/shared.d.ts +19 -0
  52. package/dist/commands/machines/shared.js +62 -0
  53. package/dist/commands/machines/shares.d.ts +16 -0
  54. package/dist/commands/machines/shares.js +69 -0
  55. package/dist/commands/machines/unshare.d.ts +19 -0
  56. package/dist/commands/machines/unshare.js +61 -0
  57. package/dist/commands/tasks/labels/create.d.ts +1 -0
  58. package/dist/commands/tasks/labels/create.js +3 -0
  59. package/dist/commands/views/create.d.ts +1 -0
  60. package/dist/commands/views/create.js +5 -0
  61. package/dist/lib/agent-home-workspace.d.ts +26 -0
  62. package/dist/lib/agent-home-workspace.js +174 -0
  63. package/dist/lib/agentic-stream.d.ts +135 -0
  64. package/dist/lib/agentic-stream.js +341 -6
  65. package/dist/lib/auth-storage.d.ts +4 -0
  66. package/dist/lib/auth-storage.js +33 -0
  67. package/dist/lib/computer-adoption.d.ts +55 -0
  68. package/dist/lib/computer-adoption.js +67 -0
  69. package/dist/lib/computer-consent.d.ts +13 -0
  70. package/dist/lib/computer-consent.js +33 -0
  71. package/dist/lib/computer-files.d.ts +239 -0
  72. package/dist/lib/computer-files.js +707 -0
  73. package/dist/lib/dedicated-copy.d.ts +45 -9
  74. package/dist/lib/dedicated-copy.js +141 -44
  75. package/dist/lib/dedicated-machines.d.ts +49 -0
  76. package/dist/lib/dedicated-machines.js +97 -6
  77. package/dist/lib/keychain.d.ts +1 -0
  78. package/dist/lib/keychain.js +67 -14
  79. package/dist/lib/label-scope.d.ts +12 -0
  80. package/dist/lib/label-scope.js +15 -1
  81. package/dist/lib/machine-audit.d.ts +34 -0
  82. package/dist/lib/machine-audit.js +50 -0
  83. package/dist/lib/machine-grants.d.ts +70 -0
  84. package/dist/lib/machine-grants.js +47 -0
  85. package/dist/lib/machine-services.d.ts +64 -0
  86. package/dist/lib/machine-services.js +60 -0
  87. package/dist/lib/refresh.d.ts +7 -0
  88. package/dist/lib/refresh.js +2 -1
  89. package/dist/lib/task-view-render.d.ts +2 -0
  90. package/dist/lib/task-view-render.js +1 -1
  91. package/dist/lib/views/vocabulary.d.ts +1 -1
  92. package/dist/lib/views/vocabulary.js +2 -1
  93. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.d.ts +955 -0
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.js +1070 -0
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +7 -0
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.d.ts +955 -0
  97. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.js +1051 -0
  98. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +7 -0
  99. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  100. package/dist/node_modules/@skrr-ai/data-provider/index.js +22143 -20728
  101. package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
  102. package/oclif.manifest.json +18855 -17260
  103. package/package.json +2 -2
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const base_command_1 = require("../../base-command");
5
+ const machines_1 = require("../../lib/machines");
6
+ const machine_grants_1 = require("../../lib/machine-grants");
7
+ /**
8
+ * `skrr machines unshare` — revoke one person's grant on a machine (design §9;
9
+ * OSK-13558). Revocation bumps the machine's grant-set version, so its daemon
10
+ * detaches that person's attachments and releases any lease they hold when
11
+ * the new set arrives. Reversible — `machines share` grants again.
12
+ */
13
+ class MachinesUnshare extends base_command_1.BaseCommand {
14
+ static description = 'Stop sharing a machine with a person — detaches their surfaces and releases their control lease';
15
+ static examples = [
16
+ '<%= config.bin %> machines unshare 03548116 68db4b2f0b4f5a9c1d2e3f40',
17
+ ];
18
+ static args = {
19
+ machine: core_1.Args.string({
20
+ ignoreStdin: true,
21
+ description: 'Machine id, uuid, daemon id, name, or a unique prefix',
22
+ required: true,
23
+ }),
24
+ user: core_1.Args.string({
25
+ ignoreStdin: true,
26
+ description: 'The user id of the person to stop sharing with',
27
+ required: true,
28
+ }),
29
+ };
30
+ static flags = {
31
+ json: core_1.Flags.boolean({ description: 'Output as JSON' }),
32
+ };
33
+ async run() {
34
+ this.requireAuth();
35
+ const { args, flags } = await this.parse(MachinesUnshare);
36
+ let target;
37
+ try {
38
+ target = (0, machines_1.resolveMachineRef)(await (0, machines_1.listMachines)(), args.machine);
39
+ }
40
+ catch (err) {
41
+ this.handleApiError(err);
42
+ }
43
+ if (!target) {
44
+ this.error('Machine not found.', { exit: 1 });
45
+ }
46
+ let result;
47
+ try {
48
+ result = await (0, machine_grants_1.unshareMachine)(target.id, args.user);
49
+ }
50
+ catch (err) {
51
+ this.handleApiError(err);
52
+ }
53
+ if (flags.json) {
54
+ this.log(JSON.stringify(result, null, 2));
55
+ return;
56
+ }
57
+ const label = target.preferences?.name || target.name || target.id;
58
+ this.log(`Stopped sharing ${label} with ${args.user}.`);
59
+ }
60
+ }
61
+ exports.default = MachinesUnshare;
@@ -3,6 +3,7 @@ export default class TasksLabelsCreate extends BaseCommand {
3
3
  static description: string;
4
4
  static examples: string[];
5
5
  static flags: {
6
+ 'allow-duplicate-name': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
6
7
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
7
8
  'from-json': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
9
  name: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -4,6 +4,7 @@ const core_1 = require("@oclif/core");
4
4
  const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const base_command_1 = require("../../../base-command");
6
6
  const prompt_1 = require("../../../lib/prompt");
7
+ const label_scope_1 = require("../../../lib/label-scope");
7
8
  // Accept both `#e11d48` and `e11d48` forms; the server's Zod regex
8
9
  // (`api/server/routes/tasks.js:206`) is `/^[0-9a-fA-F]{6}$/` — NO leading
9
10
  // `#`. We normalize before sending so users can pass either form.
@@ -24,6 +25,7 @@ class TasksLabelsCreate extends base_command_1.BaseCommand {
24
25
  color: core_1.Flags.string({
25
26
  description: 'Label color as 6-digit hex, e.g. e11d48 (leading # is accepted and stripped)',
26
27
  }),
28
+ ...label_scope_1.allowDuplicateNameFlags,
27
29
  };
28
30
  async run() {
29
31
  this.requireAuth();
@@ -38,6 +40,7 @@ class TasksLabelsCreate extends base_command_1.BaseCommand {
38
40
  const payload = (0, prompt_1.mergeDefined)(jsonBody, {
39
41
  name: flags.name,
40
42
  color: flags.color,
43
+ allowDuplicateName: flags['allow-duplicate-name'] ? true : undefined,
41
44
  });
42
45
  if (!payload.name || typeof payload.name !== 'string') {
43
46
  this.error('A label name is required. Pass --name "..." or include it in --from-json.', {
@@ -14,6 +14,7 @@ export default class ViewsCreate extends BaseCommand {
14
14
  priority: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
15
15
  assignee: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
16
16
  label: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
+ 'label-match': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
18
  goal: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
19
  project: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
20
  search: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -35,6 +35,10 @@ class ViewsCreate extends base_command_1.BaseCommand {
35
35
  }),
36
36
  assignee: core_1.Flags.string({ multiple: true, description: 'Filter by agent or user ID' }),
37
37
  label: core_1.Flags.string({ multiple: true, description: 'Filter by label ID' }),
38
+ 'label-match': core_1.Flags.string({
39
+ options: [...vocabulary_1.VIEW_LABEL_MATCH_VALUES],
40
+ description: 'How --label values combine: any (default) or all',
41
+ }),
38
42
  goal: core_1.Flags.string({ multiple: true, description: 'Filter by goal ID' }),
39
43
  project: core_1.Flags.string({ multiple: true, description: 'Filter by project ID' }),
40
44
  search: core_1.Flags.string({ description: 'Filter by title, identifier, or description' }),
@@ -67,6 +71,7 @@ class ViewsCreate extends base_command_1.BaseCommand {
67
71
  priorities: flags.priority,
68
72
  assignees: flags.assignee,
69
73
  labels: flags.label,
74
+ labelMatch: flags['label-match'],
70
75
  goalIds: flags.goal,
71
76
  projectIds: flags.project,
72
77
  search: flags.search,
@@ -17,3 +17,29 @@
17
17
  export declare function applyAgentHomeWorkspaceDefault(agentId: string, lookup: (agentId: string) => Promise<{
18
18
  workspaceId?: string | null;
19
19
  } | null | undefined>, env?: NodeJS.ProcessEnv): Promise<string | null>;
20
+ /**
21
+ * The read half of `applyAgentHomeWorkspaceDefault`: the workspace a turn
22
+ * would default to, WITHOUT setting it.
23
+ *
24
+ * Split so `agents chat` can start the agent read at the same time as its
25
+ * other pre-flight reads (`--task`, `--thread-root`, `--local-skill`) instead
26
+ * of after them — each is a full round trip — while those reads still go out
27
+ * exactly as before, with no workspace header. The default is committed only
28
+ * right before the turn is started.
29
+ */
30
+ export declare function lookupAgentHomeWorkspaceDefault(agentId: string, lookup: (agentId: string) => Promise<{
31
+ workspaceId?: string | null;
32
+ } | null | undefined>, env?: NodeJS.ProcessEnv): Promise<string | null>;
33
+ /**
34
+ * The write half: apply a looked-up default, unless something chose a
35
+ * workspace in the meantime (that choice still wins) or this is an autonomous
36
+ * Agent process.
37
+ */
38
+ export declare function commitAgentHomeWorkspaceDefault(workspaceId: string | null, env?: NodeJS.ProcessEnv): string | null;
39
+ /** Does this origin's server default a headerless chat turn's workspace itself? */
40
+ export declare function serverAppliesAgentHomeDefault(cacheDir: string | undefined, baseURL: string | undefined, now?: number): boolean;
41
+ /**
42
+ * Record what an acceptance says about the server, and return it. Writes only
43
+ * when the verdict changes or the observation is getting old.
44
+ */
45
+ export declare function recordAgentHomeDefaultObservation(cacheDir: string | undefined, baseURL: string | undefined, accepted: unknown, now?: number): 'supported' | 'unsupported';
@@ -1,6 +1,45 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.applyAgentHomeWorkspaceDefault = applyAgentHomeWorkspaceDefault;
37
+ exports.lookupAgentHomeWorkspaceDefault = lookupAgentHomeWorkspaceDefault;
38
+ exports.commitAgentHomeWorkspaceDefault = commitAgentHomeWorkspaceDefault;
39
+ exports.serverAppliesAgentHomeDefault = serverAppliesAgentHomeDefault;
40
+ exports.recordAgentHomeDefaultObservation = recordAgentHomeDefaultObservation;
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
4
43
  /**
5
44
  * The workspace an `agents chat` turn runs in when the caller named none.
6
45
  *
@@ -18,6 +57,19 @@ exports.applyAgentHomeWorkspaceDefault = applyAgentHomeWorkspaceDefault;
18
57
  * daemon session.
19
58
  */
20
59
  async function applyAgentHomeWorkspaceDefault(agentId, lookup, env = process.env) {
60
+ return commitAgentHomeWorkspaceDefault(await lookupAgentHomeWorkspaceDefault(agentId, lookup, env), env);
61
+ }
62
+ /**
63
+ * The read half of `applyAgentHomeWorkspaceDefault`: the workspace a turn
64
+ * would default to, WITHOUT setting it.
65
+ *
66
+ * Split so `agents chat` can start the agent read at the same time as its
67
+ * other pre-flight reads (`--task`, `--thread-root`, `--local-skill`) instead
68
+ * of after them — each is a full round trip — while those reads still go out
69
+ * exactly as before, with no workspace header. The default is committed only
70
+ * right before the turn is started.
71
+ */
72
+ async function lookupAgentHomeWorkspaceDefault(agentId, lookup, env = process.env) {
21
73
  if (env.OVERSKY_AUTONOMOUS_SESSION === '1')
22
74
  return null;
23
75
  if (env.OVERSKY_WORKSPACE_ID?.trim())
@@ -32,8 +84,130 @@ async function applyAgentHomeWorkspaceDefault(agentId, lookup, env = process.env
32
84
  return null;
33
85
  }
34
86
  const workspaceId = typeof agent?.workspaceId === 'string' ? agent.workspaceId.trim() : '';
87
+ return workspaceId || null;
88
+ }
89
+ /**
90
+ * The write half: apply a looked-up default, unless something chose a
91
+ * workspace in the meantime (that choice still wins) or this is an autonomous
92
+ * Agent process.
93
+ */
94
+ function commitAgentHomeWorkspaceDefault(workspaceId, env = process.env) {
35
95
  if (!workspaceId)
36
96
  return null;
97
+ if (env.OVERSKY_AUTONOMOUS_SESSION === '1')
98
+ return null;
99
+ if (env.OVERSKY_WORKSPACE_ID?.trim())
100
+ return null;
37
101
  env.OVERSKY_WORKSPACE_ID = workspaceId;
38
102
  return workspaceId;
39
103
  }
104
+ /**
105
+ * OSK-13599 — whether the server this CLI talks to chooses the agent-home
106
+ * workspace itself.
107
+ *
108
+ * A server that does (it defaults a headerless `POST /api/agentic/chat` to the
109
+ * agent's CURRENT home workspace, under exactly the checks the header would
110
+ * pass) makes the pre-flight agent read above redundant, and that read is a
111
+ * full round trip (~480ms at production RTT) on every turn.
112
+ *
113
+ * What is remembered is a fact about the SERVER, never the agent's workspace:
114
+ * agents move between workspaces, so caching their workspace could start a turn
115
+ * in the wrong one, while "this origin applies the default" stays true until
116
+ * the server is rolled back. The evidence is the acceptance itself — a server
117
+ * that applies the default always answers with a `workspaceSource` key (null
118
+ * included); an older one never does. So:
119
+ *
120
+ * - first turn against an origin: no observation → the agent read happens,
121
+ * exactly as before; the acceptance records the observation;
122
+ * - later turns: the read is skipped and the server chooses;
123
+ * - a rollback: the acceptance lacks the key, the observation is dropped and
124
+ * the NEXT turn reads again. The one turn in between ran with no workspace
125
+ * (the pre-OSK-9937 behaviour), which the command reports on stderr.
126
+ *
127
+ * Best-effort everywhere: an unreadable or unwritable cache is "not observed",
128
+ * which is today's behaviour, never an error.
129
+ */
130
+ const SERVER_FEATURES_FILE = 'server-features.json';
131
+ /** Re-confirm at most this often, so a supported server is not a write per turn. */
132
+ const OBSERVATION_REFRESH_MS = 24 * 60 * 60 * 1000;
133
+ /** An observation nobody has re-confirmed for this long is not trusted. */
134
+ const OBSERVATION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
135
+ function originOf(baseURL) {
136
+ try {
137
+ return new URL(baseURL).origin;
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ }
143
+ function featuresPath(cacheDir) {
144
+ return path.join(cacheDir, SERVER_FEATURES_FILE);
145
+ }
146
+ function readFeatures(cacheDir) {
147
+ try {
148
+ const parsed = JSON.parse(fs.readFileSync(featuresPath(cacheDir), 'utf8'));
149
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
150
+ ? parsed
151
+ : {};
152
+ }
153
+ catch {
154
+ return {};
155
+ }
156
+ }
157
+ /** Does this origin's server default a headerless chat turn's workspace itself? */
158
+ function serverAppliesAgentHomeDefault(cacheDir, baseURL, now = Date.now()) {
159
+ if (!cacheDir || !baseURL)
160
+ return false;
161
+ const origin = originOf(baseURL);
162
+ if (!origin)
163
+ return false;
164
+ const seen = readFeatures(cacheDir)[origin];
165
+ if (seen?.agentHomeWorkspaceDefault !== true)
166
+ return false;
167
+ const at = Date.parse(seen.observedAt);
168
+ return Number.isFinite(at) && now - at <= OBSERVATION_MAX_AGE_MS;
169
+ }
170
+ /**
171
+ * Record what an acceptance says about the server, and return it. Writes only
172
+ * when the verdict changes or the observation is getting old.
173
+ */
174
+ function recordAgentHomeDefaultObservation(cacheDir, baseURL, accepted, now = Date.now()) {
175
+ const supported = !!accepted && typeof accepted === 'object' && 'workspaceSource' in accepted;
176
+ const verdict = supported ? 'supported' : 'unsupported';
177
+ if (!cacheDir || !baseURL)
178
+ return verdict;
179
+ const origin = originOf(baseURL);
180
+ if (!origin)
181
+ return verdict;
182
+ try {
183
+ const features = readFeatures(cacheDir);
184
+ const previous = features[origin];
185
+ if (!supported && !previous)
186
+ return verdict;
187
+ const previousAt = previous ? Date.parse(previous.observedAt) : NaN;
188
+ if (supported &&
189
+ previous?.agentHomeWorkspaceDefault === true &&
190
+ Number.isFinite(previousAt) &&
191
+ now - previousAt < OBSERVATION_REFRESH_MS) {
192
+ return verdict;
193
+ }
194
+ if (supported) {
195
+ features[origin] = {
196
+ agentHomeWorkspaceDefault: true,
197
+ observedAt: new Date(now).toISOString(),
198
+ };
199
+ }
200
+ else {
201
+ delete features[origin];
202
+ }
203
+ fs.mkdirSync(cacheDir, { recursive: true, mode: 0o700 });
204
+ const file = featuresPath(cacheDir);
205
+ const tmp = `${file}.${process.pid}.${now}.tmp`;
206
+ fs.writeFileSync(tmp, JSON.stringify(features), { mode: 0o600 });
207
+ fs.renameSync(tmp, file);
208
+ }
209
+ catch {
210
+ // Best-effort: the next turn simply reads the agent again.
211
+ }
212
+ return verdict;
213
+ }
@@ -20,6 +20,12 @@ export type AgenticStreamResult = {
20
20
  responseMessageId?: string;
21
21
  text: string;
22
22
  status: 'completed' | 'aborted' | 'failed';
23
+ /**
24
+ * The run first failed as handed over (a deploy replaced the relaying server)
25
+ * and the machine's completed reply was then repaired onto the record
26
+ * (OSK-13543). Absent on every ordinary run.
27
+ */
28
+ repaired?: true;
23
29
  lastEvent?: AgenticStreamEvent;
24
30
  };
25
31
  /**
@@ -45,7 +51,82 @@ export type ReconciledTerminal = {
45
51
  text?: string;
46
52
  /** The recorded failure reason — `metadata.agentic.error` on the message. */
47
53
  error?: string;
54
+ /**
55
+ * The failure is a hand-over, not an ending (OSK-13543): the row is failed
56
+ * with {@link DAEMON_HANDOVER_REASON}, which the server itself treats as
57
+ * "outcome pending the machine's own turn-end".
58
+ */
59
+ handedOver?: boolean;
60
+ };
61
+ /**
62
+ * OSK-13517 / OSK-13543 — the one failure that is not an ending.
63
+ *
64
+ * A deploy replaced the server RELAYING a local-daemon turn while the daemon
65
+ * kept running it. The server saves the turn `error: true` with this reason
66
+ * (`metadata.agentic.errorSubtype`), keeps the conversation quarantined, and
67
+ * converges the row later from the machine's own turn-end: completed →
68
+ * repaired (`error: false`, `metadata.agentic.deliveryRepair`), anything else →
69
+ * settled failed (`errorSubtype: worker_replaced_daemon_failed`).
70
+ *
71
+ * Mirrors `WORKER_REPLACED_DAEMON_RUNNING_REASON` in
72
+ * `api/server/services/AgentWorker/workerReplacedNotice.js` and the code
73
+ * beside it in `daemonHandlers.js` / `conversationAdmissionClaim.js`; a parity
74
+ * spec reads those files so the two cannot drift.
75
+ */
76
+ export declare const WORKER_REPLACED_CODE = "AGENT_WORKER_REPLACED";
77
+ export declare const DAEMON_HANDOVER_REASON = "worker_replaced_daemon_running";
78
+ /**
79
+ * Is this terminal event the hand-over failure, and nothing else?
80
+ *
81
+ * The server sends it on two paths with different shapes, so the reason is
82
+ * read wherever either puts it:
83
+ * - the QueueEvents backstop (`queue.js`): `{code: 'AGENT_WORKER_REPLACED',
84
+ * errorType: 'worker_replaced_daemon_running', errors}`;
85
+ * - the worker's own result (`buildExecutionErrorEvent`): `errorType` and
86
+ * `reason` both carry the daemon reason, `code` only when the result had one.
87
+ * The plain deploy failure shares the CODE (`errorType: 'worker_replaced'`, the
88
+ * turn really ended), so the code alone must never qualify, and a code that
89
+ * is present but different disqualifies.
90
+ */
91
+ export declare function isDaemonHandoverFailure(event: Record<string, unknown> | null | undefined): boolean;
92
+ /** What the durable record says about a handed-over turn, read while waiting. */
93
+ export type HandoverReading =
94
+ /** Still failed with the hand-over reason, or not yet written: keep waiting. */
95
+ {
96
+ state: 'pending';
97
+ }
98
+ /** The machine finished and the server repaired the row. */
99
+ | {
100
+ state: 'repaired';
101
+ text: string;
102
+ repairedAt?: string;
103
+ }
104
+ /** The machine's turn ended without finishing, and the server settled it so. */
105
+ | {
106
+ state: 'failed';
107
+ error: string;
108
+ text?: string;
109
+ };
110
+ export type HandoverWaitOptions = {
111
+ /** Read the persisted response message. A throw or null is "no evidence". */
112
+ read: () => Promise<HandoverReading | null>;
113
+ /** The response message the repair notification names. */
114
+ responseMessageId?: string;
115
+ conversationId?: string;
116
+ /** How long to wait for the machine's reply. 0 disables the wait. */
117
+ maxWaitMs: number;
118
+ /** Fallback re-read cadence when no notification arrives. Default 15s. */
119
+ pollIntervalMs?: number;
120
+ /**
121
+ * Where the human status line goes when stdout is reserved for the final
122
+ * result (`--no-stream`). Default stderr.
123
+ */
124
+ statusOutput?: NodeJS.WritableStream;
48
125
  };
126
+ /** Default fallback re-read cadence while waiting on a handed-over turn. */
127
+ export declare const HANDOVER_POLL_INTERVAL_MS = 15000;
128
+ /** `600000` → `10m`, `90000` → `1m30s`, `45000` → `45s`. */
129
+ export declare function formatWaitDuration(ms: number): string;
49
130
  /**
50
131
  * Write exactly one complete JSON value on one line.
51
132
  *
@@ -106,6 +187,12 @@ export type AgenticStreamOptions = {
106
187
  * owns that stream and its contract does not include diagnostics.
107
188
  */
108
189
  debug?: (line: string) => void;
190
+ /**
191
+ * OSK-13543 — wait for a handed-over local-daemon turn instead of ending on
192
+ * its failure. See {@link isDaemonHandoverFailure}. Omitted: today's
193
+ * behaviour, every `execution:error` settles the run at once.
194
+ */
195
+ awaitHandover?: HandoverWaitOptions;
109
196
  /** Silence before the first durable-record probe. Default 20s. */
110
197
  idleTerminalProbeMs?: number;
111
198
  /** How long to keep trying to attach before reporting a detach. Default 30s. */
@@ -493,6 +580,11 @@ export declare class AgenticStreamClient {
493
580
  * a reconciled detach report the real outcome instead of the detach.
494
581
  */
495
582
  private terminalResult;
583
+ /**
584
+ * A handed-over turn being waited on (OSK-13543): the failure event that
585
+ * started the wait, and the timers bounding it. Null otherwise.
586
+ */
587
+ private handover;
496
588
  private resolve;
497
589
  private reject;
498
590
  /** The terminal result, or null while the run is still in flight. */
@@ -542,6 +634,44 @@ export declare class AgenticStreamClient {
542
634
  probeTerminal(reason: string): Promise<boolean>;
543
635
  cancel(reason?: string): void;
544
636
  close(): void;
637
+ /**
638
+ * True while a handed-over turn is being waited on (OSK-13543). A Ctrl-C in
639
+ * this state stops the wait; it cannot cancel the machine's turn.
640
+ */
641
+ awaitingHandover(): boolean;
642
+ /**
643
+ * OSK-13543 — the run failed because a deploy replaced the server relaying a
644
+ * turn the machine is still running. That failure is the server's PROVISIONAL
645
+ * verdict: it keeps the conversation held and rewrites the row from the
646
+ * machine's own turn-end. Ending here told a CLI user "failed" about a turn
647
+ * whose answer the web showed ninety seconds later.
648
+ *
649
+ * So keep the stream, the user-room subscription and the envelope replay
650
+ * open, and settle on what the RECORD converges to: repaired → completed,
651
+ * settled failed → failed with the machine's reason, neither within the
652
+ * bound → the original failure. The record, not the notification, decides:
653
+ * a notification only says "read again", so a lost or duplicated one costs
654
+ * at most a poll interval.
655
+ *
656
+ * Returns false (and changes nothing) when the run cannot wait: no hand-over
657
+ * reader configured, a zero bound, or a run already settling.
658
+ */
659
+ private beginHandoverWait;
660
+ /** Ask the record again; coalesces with a read already in flight. */
661
+ private requestHandoverRead;
662
+ /** One read of the record. True when it settled the run. */
663
+ private readHandover;
664
+ /** The bound ran out: one last look, then the original failure. */
665
+ private expireHandover;
666
+ private stopHandoverTimers;
667
+ private endHandover;
668
+ /**
669
+ * The server's "this row was rewritten" notice (`lateDeliveryRepair`
670
+ * `notifyViewersMessageRewritten`), delivered to every socket of the user
671
+ * through the `user:<id>` room this socket joins on connect. A hint to read,
672
+ * never a verdict.
673
+ */
674
+ private onConversationUpdate;
545
675
  private debug;
546
676
  /**
547
677
  * Record a hole being passed over — the moment replay has given up on it.
@@ -622,6 +752,11 @@ export declare class AgenticStreamClient {
622
752
  */
623
753
  private emitToolEvent;
624
754
  private emit;
755
+ /**
756
+ * Human-mode lines for the hand-over wait. Returns true when the event is
757
+ * fully rendered here; false lets the ordinary rendering continue.
758
+ */
759
+ private renderHandoverStatus;
625
760
  private finish;
626
761
  private finishAfterReplay;
627
762
  /**