newmark-agent 0.3.7 → 0.3.10

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.
@@ -401,7 +401,7 @@ async function runAgentKernel(agent) {
401
401
  throw new ProviderRunError(normalizePublicProviderError(lastTurn.errorMessage || lastTurn.text, [agent.activeModelConfig()?.api_key]));
402
402
  }
403
403
  const lastAssistant = lastTurn.text;
404
- if (agent.mode === 'goal' && agent.goal && agent.goal.checkComplete(lastAssistant)) {
404
+ if (agent.mode === 'goal' && agent.goal && !agent.goal.paused && agent.goal.checkComplete(lastAssistant)) {
405
405
  agent.markGoalComplete();
406
406
  if (!tokens.some(token => token.type === 'text' && /goal complete/i.test(token.text || ''))) {
407
407
  tokens.push({ type: 'text', text: '\n[Goal Complete]' });
@@ -0,0 +1,28 @@
1
+ export interface ArchivedCompressionEntry {
2
+ id: string;
3
+ at: string;
4
+ summary: string;
5
+ messages: Array<Record<string, unknown>>;
6
+ foldedEntries: number;
7
+ foldedChars: number;
8
+ model: string;
9
+ fallback: boolean;
10
+ }
11
+ /**
12
+ * Append-only cold storage for folded context segments evicted from the small
13
+ * in-state hot cache. The archive is never injected into a model request;
14
+ * callers must explicitly search/read/restore one bounded segment.
15
+ */
16
+ export declare class CompressionHistoryArchive {
17
+ private readonly rootPath;
18
+ constructor(rootPath: string);
19
+ private file;
20
+ private append;
21
+ archive(scopeKey: string, entry: ArchivedCompressionEntry): void;
22
+ markRestored(scopeKey: string, id: string): void;
23
+ /** Replays the append-only ledger and returns only currently restorable entries. */
24
+ activeEntries(scopeKey: string): ArchivedCompressionEntry[];
25
+ /** Includes restored/tombstoned ids so a restart never reuses an archive id. */
26
+ maxNumericId(scopeKey: string): number;
27
+ }
28
+ //# sourceMappingURL=compressionHistoryArchive.d.ts.map
@@ -0,0 +1,131 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CompressionHistoryArchive = void 0;
37
+ const crypto = __importStar(require("crypto"));
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ /**
41
+ * Append-only cold storage for folded context segments evicted from the small
42
+ * in-state hot cache. The archive is never injected into a model request;
43
+ * callers must explicitly search/read/restore one bounded segment.
44
+ */
45
+ class CompressionHistoryArchive {
46
+ rootPath;
47
+ constructor(rootPath) {
48
+ this.rootPath = rootPath;
49
+ }
50
+ file(scopeKey) {
51
+ const digest = crypto.createHash('sha256').update(scopeKey).digest('hex');
52
+ return path.join(this.rootPath, '.newmark-context-v2', 'compression-history', `${digest}.jsonl`);
53
+ }
54
+ append(scopeKey, event) {
55
+ const file = this.file(scopeKey);
56
+ fs.mkdirSync(path.dirname(file), { recursive: true });
57
+ fs.appendFileSync(file, `${JSON.stringify(event)}\n`, 'utf-8');
58
+ }
59
+ archive(scopeKey, entry) {
60
+ this.append(scopeKey, {
61
+ version: 1,
62
+ type: 'fold',
63
+ at: new Date().toISOString(),
64
+ entry: { ...entry, messages: entry.messages.map(message => ({ ...message })) },
65
+ });
66
+ }
67
+ markRestored(scopeKey, id) {
68
+ this.append(scopeKey, { version: 1, type: 'restore', at: new Date().toISOString(), id });
69
+ }
70
+ /** Replays the append-only ledger and returns only currently restorable entries. */
71
+ activeEntries(scopeKey) {
72
+ const file = this.file(scopeKey);
73
+ if (!fs.existsSync(file))
74
+ return [];
75
+ const active = new Map();
76
+ const lines = fs.readFileSync(file, 'utf-8').split(/\r?\n/);
77
+ for (const line of lines) {
78
+ if (!line.trim())
79
+ continue;
80
+ try {
81
+ const event = JSON.parse(line);
82
+ if (event.version !== 1)
83
+ continue;
84
+ if (event.type === 'restore') {
85
+ active.delete(String(event.id || ''));
86
+ continue;
87
+ }
88
+ if (event.type !== 'fold' || !event.entry || typeof event.entry !== 'object')
89
+ continue;
90
+ const candidate = event.entry;
91
+ if (!candidate.id || !Array.isArray(candidate.messages) || typeof candidate.summary !== 'string')
92
+ continue;
93
+ active.set(candidate.id, {
94
+ ...candidate,
95
+ messages: candidate.messages.map(message => ({ ...message })),
96
+ foldedEntries: Math.max(0, Number(candidate.foldedEntries) || candidate.messages.length),
97
+ foldedChars: Math.max(0, Number(candidate.foldedChars) || 0),
98
+ model: String(candidate.model || 'unknown'),
99
+ fallback: Boolean(candidate.fallback),
100
+ });
101
+ }
102
+ catch {
103
+ // A partial/corrupt line must not hide earlier valid append-only events.
104
+ }
105
+ }
106
+ return [...active.values()];
107
+ }
108
+ /** Includes restored/tombstoned ids so a restart never reuses an archive id. */
109
+ maxNumericId(scopeKey) {
110
+ const file = this.file(scopeKey);
111
+ if (!fs.existsSync(file))
112
+ return 0;
113
+ let max = 0;
114
+ for (const line of fs.readFileSync(file, 'utf-8').split(/\r?\n/)) {
115
+ if (!line.trim())
116
+ continue;
117
+ try {
118
+ const event = JSON.parse(line);
119
+ const entry = event.entry && typeof event.entry === 'object' ? event.entry : null;
120
+ const id = String(entry?.id || event.id || '');
121
+ max = Math.max(max, Number(id.replace(/^ctx-cache-/, '')) || 0);
122
+ }
123
+ catch {
124
+ // Ignore only the malformed line; valid earlier ids remain authoritative.
125
+ }
126
+ }
127
+ return max;
128
+ }
129
+ }
130
+ exports.CompressionHistoryArchive = CompressionHistoryArchive;
131
+ //# sourceMappingURL=compressionHistoryArchive.js.map
@@ -911,6 +911,9 @@ function defaultConfig() {
911
911
  auto_compress: { _description: "Auto-compress history", _type: "boolean", value: true },
912
912
  compress_threshold_chars: { _description: "Compression threshold", _type: "integer", value: 80000 },
913
913
  keep_recent_messages: { _description: "Keep recent messages", _type: "integer", value: 10 },
914
+ preserve_recent_messages: { _description: "dev-0.3.8 protected recent-message zone for context_history_manage (0 disables)", _type: "integer", value: 5 },
915
+ compression_cache_max: { _description: "dev-0.3.8 max folded-segment cache entries retained for restore/search", _type: "integer", value: 8 },
916
+ compression_archive_enabled: { _description: "dev-0.3.9 append-only cold archive for folded segments evicted from the hot cache", _type: "boolean", value: true },
914
917
  structured_context_v2: { _description: "dev-0.3.0 structured context v2 (orchestrator + fixed order + snapshot)", _type: "boolean", value: true },
915
918
  build_history_persistence: { _description: "dev-0.3.0 append-only Build History persistence", _type: "boolean", value: true },
916
919
  branch_log_v2: { _description: "dev-0.3.0 branch long-log v2 (epoch summaries)", _type: "boolean", value: true },
@@ -198,7 +198,7 @@ export declare class ConversationKernel {
198
198
  setWorkRunExpanded(target: ConversationTargetInput, runId: string, expanded: boolean): boolean;
199
199
  setInputMode(target: ConversationTargetInput, mode: string): 'guide' | 'next';
200
200
  setMode(target: ConversationTargetInput, mode: AgentMode): AgentMode;
201
- toggleGoalPause(target: ConversationTargetInput): boolean;
201
+ toggleGoalPause(target: ConversationTargetInput): Promise<boolean>;
202
202
  clearGoal(target: ConversationTargetInput): boolean;
203
203
  updateSetting(section: string, key: string, value: unknown): void;
204
204
  runtimeState(target: ConversationTargetInput): ConversationRuntimeState | null;
@@ -212,6 +212,7 @@ export declare class ConversationKernel {
212
212
  private processTimeoutMs;
213
213
  private runtime;
214
214
  private scheduleGoalContinuation;
215
+ private startGoalDrivenBuild;
215
216
  private createRunner;
216
217
  private enqueueRootInboxWake;
217
218
  private applyOptions;
@@ -333,11 +333,26 @@ class ConversationKernel {
333
333
  runtime.options.mode = mode;
334
334
  return runner.mode;
335
335
  }
336
- toggleGoalPause(target) {
336
+ async toggleGoalPause(target) {
337
337
  const normalized = this.normalizeTarget(target);
338
- const runtime = this.findRuntime(normalized);
339
- const runner = runtime?.runner || this.createRunner(normalized);
340
- return runner.toggleGoalPause();
338
+ let runtime = this.findRuntime(normalized);
339
+ if (!runtime) {
340
+ const runner = this.createRunner(normalized);
341
+ runtime = this.runtime(normalized, {
342
+ mode: runner.mode,
343
+ model: runner.model,
344
+ intelligence: runner.intelligence,
345
+ inputMode: runner.inputMode,
346
+ engine: runner.engine,
347
+ }, runner);
348
+ }
349
+ const wasPaused = runtime.runner.isGoalPaused();
350
+ const hadGoal = !!runtime.runner.goal;
351
+ const paused = runtime.runner.toggleGoalPause();
352
+ if (hadGoal && wasPaused && !paused)
353
+ this.startGoalDrivenBuild(runtime);
354
+ this.mirrorHostIfTargetActive(runtime);
355
+ return paused;
341
356
  }
342
357
  clearGoal(target) {
343
358
  const normalized = this.normalizeTarget(target);
@@ -401,6 +416,7 @@ class ConversationKernel {
401
416
  }
402
417
  runtime.stopRequestedRunId = runtime.runId;
403
418
  runtime.forceStopArmedRunId = runtime.runId;
419
+ runtime.runner.pauseGoalForUserInterrupt();
404
420
  this.retainUnconsumedKernelMessages(runtime);
405
421
  this.deferOutstandingGuides(runtime);
406
422
  const checkpointed = this.checkpoint(runtime.target).checkpointed;
@@ -610,7 +626,7 @@ class ConversationKernel {
610
626
  return 0;
611
627
  return Math.max(1000, Math.floor(raw));
612
628
  }
613
- runtime(target, options) {
629
+ runtime(target, options, runnerOverride) {
614
630
  const existing = this.findRuntime(target);
615
631
  if (existing) {
616
632
  this.ensureRuntimeMetadata(existing, target);
@@ -618,7 +634,7 @@ class ConversationKernel {
618
634
  return existing;
619
635
  }
620
636
  const id = target.conversationId;
621
- const runner = this.createRunner(target);
637
+ const runner = runnerOverride || this.createRunner(target);
622
638
  runner.setAutomationManager(this.automation);
623
639
  this.applyOptions(runner, options);
624
640
  const runtime = {
@@ -732,8 +748,23 @@ class ConversationKernel {
732
748
  });
733
749
  }, 250);
734
750
  }
751
+ startGoalDrivenBuild(runtime) {
752
+ if (runtime.goalContinuationTimer) {
753
+ clearTimeout(runtime.goalContinuationTimer);
754
+ runtime.goalContinuationTimer = undefined;
755
+ }
756
+ const message = runtime.runner.claimGoalContinuationMessage({ force: true });
757
+ if (!message)
758
+ return;
759
+ void this.prompt(message, runtime.target, { ...runtime.options, mode: 'goal' }, 'followUp').catch(() => {
760
+ // Agent.process records and publishes the Build error.
761
+ });
762
+ }
735
763
  createRunner(target) {
736
- const runner = this.lifecycle.createRunner?.(target) || new agent_1.Agent(this.root, { actorId: this.host.runtimeActorId });
764
+ const runner = this.lifecycle.createRunner?.(target) || new agent_1.Agent(this.root, {
765
+ actorId: this.host.runtimeActorId,
766
+ runtimeLifecycleRole: this.host.runtimeLifecycleRole,
767
+ });
737
768
  if (target.workspace) {
738
769
  runner.workspace.current = {
739
770
  id: target.workspace.id,
@@ -94,11 +94,20 @@ class ElectronBrowserUseHost {
94
94
  evaluateFixed: async (script, signal) => await raceWithAbort(contents.executeJavaScriptInIsolatedWorld(BROWSER_USE_WORLD_ID, [{ code: script }], true), signal),
95
95
  clickAt: async (x, y, signal) => {
96
96
  throwIfAborted(signal);
97
+ // A webview guest can retain the DOM focus while its embedder is still
98
+ // committing a workspace/tab transition. Focus both sides and yield
99
+ // between native input events so Chromium receives a real click rather
100
+ // than a synchronously queued sequence that can be dropped by the guest.
101
+ contents.hostWebContents?.focus();
97
102
  contents.focus();
103
+ await abortableDelay(10, signal);
98
104
  const point = { x: Math.max(0, Math.round(x)), y: Math.max(0, Math.round(y)) };
99
105
  contents.sendInputEvent({ type: 'mouseMove', ...point });
106
+ await abortableDelay(10, signal);
100
107
  contents.sendInputEvent({ type: 'mouseDown', button: 'left', clickCount: 1, ...point });
108
+ await abortableDelay(10, signal);
101
109
  contents.sendInputEvent({ type: 'mouseUp', button: 'left', clickCount: 1, ...point });
110
+ await abortableDelay(10, signal);
102
111
  },
103
112
  replaceFocusedText: async (text, signal) => {
104
113
  throwIfAborted(signal);
@@ -247,7 +247,7 @@ class ElectronUtilityRuntimePool {
247
247
  }
248
248
  }
249
249
  async toggleGoalPause(target) {
250
- const entry = await this.acquireExisting((0, conversationTarget_1.normalizeConversationTarget)(target));
250
+ const entry = await this.acquire((0, conversationTarget_1.normalizeConversationTarget)(target));
251
251
  if (!entry?.client.toggleGoalPause)
252
252
  return null;
253
253
  try {
@@ -630,12 +630,11 @@ class ElectronUtilityRuntimePool {
630
630
  }
631
631
  const finalized = !!kernelForce && kernelForce.action === 'force';
632
632
  const checkpointed = finalized ? (kernelForce.checkpointed || intent.checkpointed) : intent.checkpointed;
633
- if (!finalized) {
634
- // The worker event loop is wedged: hard-kill the process tree without
635
- // starting a replacement. The first-stop checkpoint preserves the
636
- // conversation; only the latest run is discarded.
637
- await entry.client.forceStop();
638
- }
633
+ // The acknowledgement only gives the kernel a chance to persist its
634
+ // force_interrupted settlement. A second Stop still owns a process-level
635
+ // termination boundary and must leave the target runtime down until the
636
+ // next prompt, even when the worker acknowledged the force request.
637
+ await entry.client.forceStop();
639
638
  entry.lastSnapshot = null;
640
639
  entry.workEvents = [];
641
640
  entry.lastRunId = '';
@@ -0,0 +1,23 @@
1
+ export type RuntimeLifecycleRole = 'main' | 'utility' | 'wsl';
2
+ export interface RuntimeLifecycleState {
3
+ role: RuntimeLifecycleRole;
4
+ ownerId: string;
5
+ pid: number;
6
+ startedAt: string;
7
+ previousOwnerAlive: boolean;
8
+ unexpectedExit: boolean;
9
+ active: true;
10
+ }
11
+ export declare function isRuntimeProcessAlive(pid: number): boolean;
12
+ /**
13
+ * Claim this process/role as the current runtime owner.
14
+ *
15
+ * A frontend cold runner in the same process gets the same in-memory claim and
16
+ * therefore does not look like a restart. A genuinely new process sees the
17
+ * previous active claim and can pause orphaned Goal/Flow state once.
18
+ */
19
+ export declare function beginRuntimeLifecycle(root: string, role?: RuntimeLifecycleRole): RuntimeLifecycleState;
20
+ /** Mark only this owner clean; a hard kill leaves the active marker intact. */
21
+ export declare function markRuntimeLifecycleClean(root: string, role?: RuntimeLifecycleRole): void;
22
+ export declare function runtimeLifecycleState(root: string, role?: RuntimeLifecycleRole): RuntimeLifecycleState | null;
23
+ //# sourceMappingURL=runtimeLifecycle.d.ts.map
@@ -0,0 +1,146 @@
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
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.isRuntimeProcessAlive = isRuntimeProcessAlive;
37
+ exports.beginRuntimeLifecycle = beginRuntimeLifecycle;
38
+ exports.markRuntimeLifecycleClean = markRuntimeLifecycleClean;
39
+ exports.runtimeLifecycleState = runtimeLifecycleState;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const crypto_1 = require("crypto");
43
+ const processStates = new Map();
44
+ function stateKey(root, role) {
45
+ return `${path.resolve(root)}\u0000${role}`;
46
+ }
47
+ function statePath(root, role, ownerId = '') {
48
+ return path.join(root, '.newmark-runtime', ownerId ? `lifecycle-${role}-${ownerId}.json` : `lifecycle-${role}.json`);
49
+ }
50
+ function readState(file) {
51
+ try {
52
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
53
+ return parsed && typeof parsed === 'object' ? parsed : null;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ function isRuntimeProcessAlive(pid) {
60
+ const candidate = Math.floor(Number(pid) || 0);
61
+ if (candidate <= 0)
62
+ return false;
63
+ try {
64
+ process.kill(candidate, 0);
65
+ return true;
66
+ }
67
+ catch {
68
+ return false;
69
+ }
70
+ }
71
+ function writeState(root, role, state) {
72
+ const ownerId = typeof state.ownerId === 'string' ? state.ownerId : '';
73
+ const file = statePath(root, role, ownerId);
74
+ fs.mkdirSync(path.dirname(file), { recursive: true });
75
+ const temporary = `${file}.${process.pid}.${(0, crypto_1.randomUUID)()}.tmp`;
76
+ fs.writeFileSync(temporary, JSON.stringify(state, null, 2), 'utf-8');
77
+ fs.renameSync(temporary, file);
78
+ }
79
+ function readActiveStates(root, role) {
80
+ const directory = path.join(root, '.newmark-runtime');
81
+ try {
82
+ const prefix = `lifecycle-${role}`;
83
+ return fs.readdirSync(directory)
84
+ .filter(file => file.startsWith(prefix) && file.endsWith('.json'))
85
+ .map(file => readState(path.join(directory, file)))
86
+ .filter((state) => !!state && state.active === true);
87
+ }
88
+ catch {
89
+ return [];
90
+ }
91
+ }
92
+ /**
93
+ * Claim this process/role as the current runtime owner.
94
+ *
95
+ * A frontend cold runner in the same process gets the same in-memory claim and
96
+ * therefore does not look like a restart. A genuinely new process sees the
97
+ * previous active claim and can pause orphaned Goal/Flow state once.
98
+ */
99
+ function beginRuntimeLifecycle(root, role = 'main') {
100
+ const key = stateKey(root, role);
101
+ const existingProcessState = processStates.get(key);
102
+ if (existingProcessState)
103
+ return existingProcessState;
104
+ const previousStates = readActiveStates(root, role);
105
+ const previousOwnerAlive = previousStates.some(previous => isRuntimeProcessAlive(Number(previous.pid)));
106
+ const state = {
107
+ role,
108
+ ownerId: (0, crypto_1.randomUUID)(),
109
+ pid: process.pid,
110
+ startedAt: new Date().toISOString(),
111
+ previousOwnerAlive,
112
+ unexpectedExit: previousStates.length > 0 && !previousOwnerAlive,
113
+ active: true,
114
+ };
115
+ try {
116
+ writeState(root, role, state);
117
+ }
118
+ catch {
119
+ // Recovery remains conservative when the marker cannot be written. The
120
+ // persisted WorkRun timestamp still protects a live backend cold read.
121
+ }
122
+ processStates.set(key, state);
123
+ return state;
124
+ }
125
+ /** Mark only this owner clean; a hard kill leaves the active marker intact. */
126
+ function markRuntimeLifecycleClean(root, role = 'main') {
127
+ const key = stateKey(root, role);
128
+ const current = processStates.get(key);
129
+ if (!current)
130
+ return;
131
+ try {
132
+ writeState(root, role, {
133
+ ...current,
134
+ active: false,
135
+ cleanExitAt: new Date().toISOString(),
136
+ });
137
+ }
138
+ catch {
139
+ // Leaving the marker active is safer than claiming a clean exit after a
140
+ // failed durable write.
141
+ }
142
+ }
143
+ function runtimeLifecycleState(root, role = 'main') {
144
+ return processStates.get(stateKey(root, role)) || null;
145
+ }
146
+ //# sourceMappingURL=runtimeLifecycle.js.map
@@ -129,6 +129,9 @@ export interface ConversationWorkRun {
129
129
  runId: string;
130
130
  target: ConversationTarget;
131
131
  runtimeKey: string;
132
+ runtimeOwnerId?: string;
133
+ runtimeOwnerPid?: number;
134
+ runtimeLifecycleRole?: 'main' | 'utility' | 'wsl';
132
135
  status: ConversationWorkRunStatus;
133
136
  startedAt: string;
134
137
  endedAt?: string;
@@ -262,7 +262,7 @@ class WslAgentRuntimePool {
262
262
  }
263
263
  async toggleGoalPause(target) {
264
264
  const normalized = (0, conversationTarget_1.normalizeConversationTarget)(target);
265
- const entry = await this.acquireExisting(normalized);
265
+ const entry = await this.acquire(normalized);
266
266
  if (!entry?.client.toggleGoalPause)
267
267
  return null;
268
268
  try {
@@ -616,15 +616,14 @@ class WslAgentRuntimePool {
616
616
  }
617
617
  const finalized = !!kernelForce && kernelForce.action === 'force';
618
618
  const checkpointed = finalized ? (kernelForce.checkpointed || intent.checkpointed) : intent.checkpointed;
619
- if (!finalized) {
620
- // The worker event loop is wedged: hard-kill the process group without
621
- // starting a replacement. The first-stop checkpoint preserves the
622
- // conversation; only the latest run is discarded.
623
- if (entry.client.forceStopRuntimeGroup)
624
- await entry.client.forceStopRuntimeGroup();
625
- else
626
- await entry.client.forceRestartRuntimeGroup();
627
- }
619
+ // The acknowledgement only gives the kernel a chance to persist its
620
+ // force_interrupted settlement. A second Stop still owns a process-group
621
+ // termination boundary and must leave the target runtime down until the
622
+ // next prompt, even when the worker acknowledged the force request.
623
+ if (entry.client.forceStopRuntimeGroup)
624
+ await entry.client.forceStopRuntimeGroup();
625
+ else
626
+ await entry.client.forceRestartRuntimeGroup();
628
627
  entry.lastSnapshot = null;
629
628
  entry.workEvents = [];
630
629
  entry.lastRunId = '';
package/dist/main.js CHANGED
@@ -70,6 +70,7 @@ const wslAgentRuntimePool_1 = require("./core/wslAgentRuntimePool");
70
70
  const utilityHostToolRouter_1 = require("./core/utilityHostToolRouter");
71
71
  const startupPrewarm_1 = require("./core/startupPrewarm");
72
72
  const runtimeShutdown_1 = require("./core/runtimeShutdown");
73
+ const runtimeLifecycle_1 = require("./core/runtimeLifecycle");
73
74
  const compat_1 = require("./core/compat");
74
75
  const mcpManager_1 = require("./core/mcpManager");
75
76
  const APP_NAME = 'Newmark Agent';
@@ -2106,6 +2107,15 @@ else {
2106
2107
  }
2107
2108
  electron_1.app.quit();
2108
2109
  };
2110
+ const hasUnsettledGoalOrFlow = () => {
2111
+ const goalRunning = !!agent?.goal && !agent.goal.paused;
2112
+ const flowRunning = Array.from(activeFlowsByRuntimeKey.values()).some(state => !!state.abortController);
2113
+ return goalRunning || flowRunning;
2114
+ };
2115
+ const markMainLifecycleCleanIfSafe = () => {
2116
+ if (!hasUnsettledGoalOrFlow())
2117
+ (0, runtimeLifecycle_1.markRuntimeLifecycleClean)(root, 'main');
2118
+ };
2109
2119
  electron_1.app.on('will-quit', event => {
2110
2120
  if (_forceQuit)
2111
2121
  armForcedExitDeadline('will-quit');
@@ -2131,6 +2141,7 @@ else {
2131
2141
  throw error;
2132
2142
  })
2133
2143
  : undefined;
2144
+ let shutdownFailed = false;
2134
2145
  void (0, runtimeShutdown_1.runRuntimeShutdownBarrier)({
2135
2146
  operations: [
2136
2147
  legacyStop,
@@ -2139,9 +2150,12 @@ else {
2139
2150
  ],
2140
2151
  shutdownHelpers: async () => await (0, electronUtilityAgentClient_1.shutdownWindowsProcessHelpers)(2_000),
2141
2152
  }).catch(error => {
2153
+ shutdownFailed = true;
2142
2154
  console.error('[Newmark] Runtime shutdown cleanup failed:', error instanceof Error ? error.message : String(error));
2143
2155
  }).finally(() => {
2144
2156
  appExitCleanupComplete = true;
2157
+ if (!shutdownFailed)
2158
+ markMainLifecycleCleanIfSafe();
2145
2159
  electron_1.app.quit();
2146
2160
  });
2147
2161
  }
@@ -2170,6 +2184,7 @@ else {
2170
2184
  sidecarProcess = null;
2171
2185
  }
2172
2186
  (0, terminalTakeover_1.shutdownTerminalTakeoverSessions)('app-exit');
2187
+ markMainLifecycleCleanIfSafe();
2173
2188
  if (forcedExitTimer) {
2174
2189
  clearTimeout(forcedExitTimer);
2175
2190
  forcedExitTimer = null;
@@ -374,11 +374,17 @@ class ToolExecutor {
374
374
  t('linked_plan', 'Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.', { action: { type: 'string', enum: ['get', 'update'] }, markdown: { type: 'string' }, expected_revision: { type: 'number' } }, ['action']),
375
375
  t('build_history_query', 'Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query.', { history_index: { type: 'number', minimum: 1, description: '1-based historical Build Block index from the request ledger; 1 is the newest previous task.' }, run_id: { type: 'string', description: 'Exact run id returned by an earlier build_history_query result.' }, max_events: { type: 'number', minimum: 1, maximum: 200, description: 'Maximum trailing public work events; defaults to 80.' } }, []),
376
376
  t('context_compress', 'Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.', { keep_recent: { type: 'number', minimum: 2, maximum: 60, description: 'Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages.' }, force: { type: 'boolean', description: 'Compress even if the context is not yet over the automatic threshold. Defaults to false.' } }, []),
377
- t('context_history_manage', 'Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry. The displayed conversation history (what the user sees) is never modified by any action.', {
378
- action: { type: 'string', enum: ['list', 'remove', 'summarize'], description: 'list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry.' },
377
+ t('context_history_manage', 'Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. The hot cache stays bounded; evicted folded segments remain in a conversation-isolated append-only cold archive and are loaded only by explicit search/read/restore calls. Actions: list returns a bounded index of current context entries; remove deletes one current entry; summarize folds a contiguous current range; restore reinserts a folded segment when its summary marker is still present; search finds matching hot or archived segments; read returns one bounded segment without injecting the whole archive; status reports budgets, hot cache, cold archive, and the protected recent zone. The recent context tail and last user message are protected from remove/summarize unless dangerous is true.', {
378
+ action: { type: 'string', enum: ['list', 'remove', 'summarize', 'restore', 'search', 'read', 'status'], description: 'list current entries; remove one; summarize a range; restore by restore_id; search hot/cold folded segments; read one bounded folded segment; status report context budgets and storage.' },
379
379
  position: { type: 'number', minimum: 0, description: '0-based context entry index for remove, or the start of the range for summarize.' },
380
380
  to: { type: 'number', minimum: 0, description: '0-based inclusive end of the range for summarize. Defaults to position.' },
381
- limit: { type: 'number', minimum: 5, maximum: 400, description: 'Maximum context entries to list; defaults to 200.' },
381
+ limit: { type: 'number', minimum: 1, maximum: 400, description: 'Maximum context entries/messages/matches to return. Current-history list still keeps a minimum page of 5.' },
382
+ restore_id: { type: 'string', description: 'Cache id of a folded segment (from search or status) to restore into context.' },
383
+ query: { type: 'string', description: 'Case-insensitive text to search for across cached folded segments and their summaries.' },
384
+ offset: { type: 'number', minimum: 0, description: 'Message offset for read pagination.' },
385
+ content_offset: { type: 'number', minimum: 0, description: 'Character offset within the first read message, used with nextContentOffset when one message exceeds max_chars.' },
386
+ max_chars: { type: 'number', minimum: 1000, maximum: 60000, description: 'Maximum message-content characters returned by read (default 12000).' },
387
+ dangerous: { type: 'boolean', description: 'Set true to override the protected recent-message zone and allow removing/summarizing entries that include the recent context tail or the last user message.' },
382
388
  }, ['action']),
383
389
  t('question', 'Ask user a multiple-choice question', { questions: { type: 'array' } }, ['questions']),
384
390
  t('skill_download', 'Download a skill', { name: { type: 'string' }, source: { type: 'string' } }, ['name', 'source']),
@@ -41,7 +41,7 @@ exports.NATIVE_TOOL_CATALOG = [
41
41
  { name: 'linked_plan', label: 'Linked plan', description: 'Read or conservatively update the conversation-linked Markdown plan.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
42
42
  { name: 'build_history_query', label: 'Build history query', description: 'Read concrete public work details for one historical Build Block.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
43
43
  { name: 'context_compress', label: 'Context compress', description: 'Actively compress the LLM context history, leaving the displayed conversation history unchanged.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
44
- { name: 'context_history_manage', label: 'Context history manage', description: 'List, remove, or summarize entries in the LLM context history without touching the displayed conversation history.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
44
+ { name: 'context_history_manage', label: 'Context history manage', description: 'Inspect, search, restore, or fold LLM context history without touching the displayed conversation history.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
45
45
  { name: 'question', label: 'Ask question', description: 'Ask the user for structured option feedback.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
46
46
  { name: 'skill_download', label: 'Skill download', description: 'Download and install a skill.', category: 'agent', defaultEnabled: true },
47
47
  { name: 'skill', label: 'Skill', description: 'Search enabled skill metadata or load one skill body on demand.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },