@gajae-code/coding-agent 0.12.10 → 0.12.11

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.
@@ -56,9 +56,10 @@ export const NOTIFICATION_PROTOCOL_VERSION = 3;
56
56
  * adds durable provider-intent admission without changing owner, reclaim,
57
57
  * signal, or spawn authority.
58
58
  * Generation 42 applies first-class provider-settings admission to Telegram
59
- * lifecycle controls.
59
+ * lifecycle controls. Generation 43 applies identity-bound exact replacement
60
+ * cleanup shared by managed-session and daemon filesystem authority.
60
61
  */
61
- export const DAEMON_GENERATION = 42;
62
+ export const DAEMON_GENERATION = 43;
62
63
 
63
64
  /**
64
65
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 1 covers
@@ -427,6 +427,7 @@ import {
427
427
  getLatestCompactionEntry,
428
428
  getSessionMessageEntryId,
429
429
  getSessionMessageObservationId,
430
+ SessionAppendPersistenceError,
430
431
  transferSessionMessageIdentity,
431
432
  } from "./session-manager";
432
433
  import { getEntriesForInternalRead, getSessionContextForInternalRead } from "./session-manager-internal";
@@ -3899,14 +3900,19 @@ export class AgentSession {
3899
3900
  ) {
3900
3901
  const isRosterReminder = event.message.role === "custom" && event.message.customType === "irc-peer-roster";
3901
3902
  if (!isRosterReminder) {
3902
- this.#appendCustomMessageEntry(
3903
- event.message.customType,
3904
- event.message.content,
3905
- event.message.display,
3906
- event.message.details,
3907
- event.message.attribution ?? "agent",
3908
- getSessionMessageObservationId(event.message),
3909
- );
3903
+ try {
3904
+ this.#appendCustomMessageEntry(
3905
+ event.message.customType,
3906
+ event.message.content,
3907
+ event.message.display,
3908
+ event.message.details,
3909
+ event.message.attribution ?? "agent",
3910
+ getSessionMessageObservationId(event.message),
3911
+ );
3912
+ } catch (error) {
3913
+ this.agent.abort();
3914
+ throw error;
3915
+ }
3910
3916
  }
3911
3917
  } else if (
3912
3918
  event.message.role === "user" ||
@@ -3915,7 +3921,73 @@ export class AgentSession {
3915
3921
  event.message.role === "toolResult" ||
3916
3922
  event.message.role === "fileMention"
3917
3923
  ) {
3918
- this.sessionManager.appendMessage(event.message);
3924
+ try {
3925
+ this.sessionManager.appendMessage(event.message);
3926
+ } catch (error) {
3927
+ if (
3928
+ event.message.role !== "toolResult" ||
3929
+ event.message.toolName !== "todo_write" ||
3930
+ !(error instanceof SessionAppendPersistenceError) ||
3931
+ error.phase !== "current_append"
3932
+ ) {
3933
+ this.agent.abort();
3934
+ throw error;
3935
+ }
3936
+ this.agent.abort();
3937
+
3938
+ const failure = error.persistenceError.message;
3939
+ const failedEntryId = error.entryId;
3940
+ this.#syncTodoPhasesFromBranch();
3941
+ event.message.isError = true;
3942
+ event.message.content = [
3943
+ {
3944
+ type: "text",
3945
+ text: `Todo state persistence failed: ${failure}\nDo not change the payload solely because of this failure. The durable outcome is unknown; reconcile the session state before retrying or continuing.`,
3946
+ },
3947
+ ];
3948
+ event.message.details = {
3949
+ ...(event.message.details && typeof event.message.details === "object" ? event.message.details : {}),
3950
+ phases: this.getTodoPhases(),
3951
+ failureKind: "persistence",
3952
+ };
3953
+ this.agent.touchContext();
3954
+ let recovered = false;
3955
+ try {
3956
+ await this.sessionManager.recoverPersistenceFailure();
3957
+ recovered = true;
3958
+ } catch (recoveryError) {
3959
+ logger.warn("Todo persistence recovery failed", {
3960
+ error: recoveryError instanceof Error ? recoveryError.message : String(recoveryError),
3961
+ });
3962
+ }
3963
+ if (recovered) {
3964
+ const durableTodoResult = this.sessionManager
3965
+ .getBranch()
3966
+ .find(
3967
+ entry =>
3968
+ entry.id === failedEntryId &&
3969
+ entry.type === "message" &&
3970
+ entry.message.role === "toolResult" &&
3971
+ entry.message.toolName === "todo_write",
3972
+ );
3973
+ this.#syncTodoPhasesFromBranch();
3974
+ if (durableTodoResult?.type === "message" && durableTodoResult.message.role === "toolResult") {
3975
+ event.message.content = durableTodoResult.message.content;
3976
+ event.message.details = durableTodoResult.message.details;
3977
+ event.message.isError = durableTodoResult.message.isError;
3978
+ } else {
3979
+ event.message.details = {
3980
+ ...(event.message.details && typeof event.message.details === "object"
3981
+ ? event.message.details
3982
+ : {}),
3983
+ phases: this.getTodoPhases(),
3984
+ failureKind: "persistence",
3985
+ };
3986
+ this.sessionManager.appendMessage(event.message);
3987
+ this.agent.touchContext();
3988
+ }
3989
+ }
3990
+ }
3919
3991
  }
3920
3992
  }
3921
3993
 
@@ -4189,7 +4261,13 @@ export class AgentSession {
4189
4261
  if (event.message.role === "toolResult") {
4190
4262
  const { toolName, details, isError, content } = event.message as {
4191
4263
  toolName?: string;
4192
- details?: { path?: string; phases?: TodoPhase[]; report?: string; startedAt?: string };
4264
+ details?: {
4265
+ path?: string;
4266
+ phases?: TodoPhase[];
4267
+ report?: string;
4268
+ startedAt?: string;
4269
+ failureKind?: "payload_rejected" | "argument_validation" | "execution" | "persistence";
4270
+ };
4193
4271
  isError?: boolean;
4194
4272
  content?: Array<TextContent | ImageContent>;
4195
4273
  };
@@ -4202,22 +4280,34 @@ export class AgentSession {
4202
4280
  }
4203
4281
  if (toolName === "todo_write" && isError) {
4204
4282
  const errorText = content?.find(part => part.type === "text")?.text;
4283
+ const payloadRejected =
4284
+ details?.failureKind === "payload_rejected" || details?.failureKind === "argument_validation";
4205
4285
  const reminderText = [
4206
4286
  "<system-reminder>",
4207
- "todo_write failed, so todo progress is not visible to the user.",
4287
+ payloadRejected
4288
+ ? "todo_write rejected its payload. The requested todo update was not applied."
4289
+ : "todo_write did not complete because the turn aborted during runtime execution or persistence.",
4208
4290
  errorText ? `Failure: ${errorText}` : "Failure: todo_write returned an error.",
4209
- "Fix the todo payload and call todo_write again before continuing.",
4291
+ payloadRejected
4292
+ ? "Correct the payload and call todo_write again before continuing."
4293
+ : "Do not change the payload solely because of this failure. The durable outcome is unknown; reconcile the session state before retrying or continuing.",
4210
4294
  "</system-reminder>",
4211
4295
  ].join("\n");
4212
- await this.sendCustomMessage(
4213
- {
4214
- customType: "todo-write-error-reminder",
4215
- content: reminderText,
4216
- display: false,
4217
- details: { toolName, errorText },
4218
- },
4219
- { deliverAs: "nextTurn" },
4220
- );
4296
+ try {
4297
+ await this.sendCustomMessage(
4298
+ {
4299
+ customType: "todo-write-error-reminder",
4300
+ content: reminderText,
4301
+ display: false,
4302
+ details: { toolName, errorText },
4303
+ },
4304
+ { deliverAs: "nextTurn" },
4305
+ );
4306
+ } catch (error) {
4307
+ logger.warn("Todo write error reminder persistence failed", {
4308
+ error: error instanceof Error ? error.message : String(error),
4309
+ });
4310
+ }
4221
4311
  }
4222
4312
  if (toolName === "checkpoint" && !isError) {
4223
4313
  const checkpointEntryId = this.sessionManager.getLeafId();