happy-imou-cloud 2.1.28 → 2.1.30

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 (26) hide show
  1. package/dist/{BaseReasoningProcessor-DYVFvY85.mjs → BaseReasoningProcessor-C1iacoJW.mjs} +2 -2
  2. package/dist/{BaseReasoningProcessor-AsSwxX2U.cjs → BaseReasoningProcessor-Ch9R4qmn.cjs} +2 -2
  3. package/dist/{ProviderSelectionHandler-Cd_vN8wA.mjs → ProviderSelectionHandler-2siFKlgs.mjs} +2 -2
  4. package/dist/{ProviderSelectionHandler-BKfo21qx.cjs → ProviderSelectionHandler-Dx6x0Nd4.cjs} +2 -2
  5. package/dist/{api-DYS9sGJr.mjs → api-C68U-kRs.mjs} +626 -126
  6. package/dist/{api-ChV_1TP7.cjs → api-DK1gyZAZ.cjs} +626 -126
  7. package/dist/{command-ErrXrwTw.cjs → command-Cb9nikZh.cjs} +2 -2
  8. package/dist/{command-Bx9UY5D5.mjs → command-D32x08k9.mjs} +2 -2
  9. package/dist/{index-CDyeCS7U.cjs → index-BLeiCte-.cjs} +174 -32
  10. package/dist/{index-DtlrIihs.mjs → index-DQ76ZTNL.mjs} +171 -29
  11. package/dist/index.cjs +2 -2
  12. package/dist/index.mjs +2 -2
  13. package/dist/lib.cjs +1 -1
  14. package/dist/lib.d.cts +100 -0
  15. package/dist/lib.d.mts +100 -0
  16. package/dist/lib.mjs +1 -1
  17. package/dist/{registerKillSessionHandler-IlzXqONk.mjs → registerKillSessionHandler-CadrzRgP.mjs} +16 -6
  18. package/dist/{registerKillSessionHandler-CBuJR_D8.cjs → registerKillSessionHandler-DD9uUk4w.cjs} +16 -6
  19. package/dist/{runClaude-VlpAUO9C.mjs → runClaude-CkY6XYJa.mjs} +4 -4
  20. package/dist/{runClaude-D_YCDVmM.cjs → runClaude-U9sxsnU-.cjs} +4 -4
  21. package/dist/{runCodex-jisfibyM.cjs → runCodex-Beikmv-L.cjs} +134 -9
  22. package/dist/{runCodex-CL1yQHxl.mjs → runCodex-C6kV0jfX.mjs} +134 -9
  23. package/dist/{runGemini-BtBrzhkF.cjs → runGemini-BTyqf5MR.cjs} +4 -4
  24. package/dist/{runGemini-EPv-yxY4.mjs → runGemini-IEzJdhc-.mjs} +4 -4
  25. package/package.json +1 -1
  26. package/scripts/release-smoke.mjs +3 -0
@@ -38,7 +38,7 @@ function _interopNamespaceDefault(e) {
38
38
  var z__namespace = /*#__PURE__*/_interopNamespaceDefault(z);
39
39
 
40
40
  var name = "happy-imou-cloud";
41
- var version = "2.1.28";
41
+ var version = "2.1.30";
42
42
  var description = "hicloud - Imou 企业定制版。关键是 happy!移动端远程 AI 编程工具,支持 Claude Code、Codex 和 Gemini CLI";
43
43
  var author = "long.zhu";
44
44
  var license = "MIT";
@@ -1230,7 +1230,9 @@ const HappyOrgTaskContextSchema = z.z.object({
1230
1230
  taskId: z.z.string().min(1),
1231
1231
  organizationId: z.z.string().min(1),
1232
1232
  memberAgentId: z.z.string().min(1),
1233
- supervisorAgentId: z.z.string().min(1)
1233
+ supervisorAgentId: z.z.string().min(1),
1234
+ positionId: z.z.string().min(1).optional().nullable(),
1235
+ responsibilityId: z.z.string().min(1).optional().nullable()
1234
1236
  });
1235
1237
  const HappyOrgTaskControlSchema = z.z.object({
1236
1238
  action: z.z.enum(["terminate", "reopen"]),
@@ -1242,7 +1244,9 @@ const HappyOrgTaskControlSchema = z.z.object({
1242
1244
  const HappyOrgReplyContextSchema = z.z.object({
1243
1245
  dispatchId: z.z.string().min(1),
1244
1246
  scope: z.z.string().min(1),
1245
- replyTo: z.z.string().min(1)
1247
+ replyTo: z.z.string().min(1),
1248
+ positionId: z.z.string().min(1).optional().nullable(),
1249
+ responsibilityId: z.z.string().min(1).optional().nullable()
1246
1250
  });
1247
1251
  const HappyOrgDispatchBusinessAckStatusSchema = z.z.enum([
1248
1252
  "accepted",
@@ -1595,6 +1599,15 @@ const AgentMessageSchema = z.z.object({
1595
1599
  const MessageContentSchema = z.z.union([UserMessageSchema, AgentMessageSchema]);
1596
1600
 
1597
1601
  const HAPPY_ORG_LOCAL_REPO_SCHEMA_VERSION = "1.7.3-local-repo-v1";
1602
+ const HAPPY_ORG_WRITER_LOCK_SCHEMA_VERSION = "1.7.3-writer-lock-v1";
1603
+ const WRITER_LOCK_LEASE_DURATION_SECONDS = 900;
1604
+ const WRITER_LOCK_RENEW_BEFORE_SECONDS = 300;
1605
+ const WRITER_LOCK_CEO_TAKEOVER_AFTER_SECONDS = 1800;
1606
+ const WRITER_LOCK_RENEW_INTERVAL_MS = Math.max(
1607
+ (WRITER_LOCK_LEASE_DURATION_SECONDS - WRITER_LOCK_RENEW_BEFORE_SECONDS) * 1e3,
1608
+ 1e3
1609
+ );
1610
+ let writerLockSequence = 0;
1598
1611
  const TaskStatusSchema = z.z.enum([
1599
1612
  "draft",
1600
1613
  "ready",
@@ -1678,10 +1691,46 @@ const UpdateTaskRequestSchema = BaseRequestSchema.extend({
1678
1691
  expectedRevision: z.z.number().nullable(),
1679
1692
  nextState: TaskUpdatePayloadSchema
1680
1693
  });
1694
+ const AgentUpsertPayloadSchema = z.z.object({
1695
+ id: z.z.string().min(1),
1696
+ slug: z.z.string().min(1),
1697
+ role: z.z.string().min(1),
1698
+ name: z.z.string().min(1),
1699
+ sessionId: z.z.string().min(1),
1700
+ sessionTitle: z.z.string().min(1),
1701
+ runtimePath: z.z.string().nullable(),
1702
+ workingDirectoryRelativePath: z.z.string().nullable().optional(),
1703
+ machineId: z.z.string().nullable(),
1704
+ backend: z.z.string().nullable(),
1705
+ supervisorAgentId: z.z.string().nullable(),
1706
+ createdAt: z.z.number(),
1707
+ updatedAt: z.z.number()
1708
+ });
1709
+ const PositionUpsertPayloadSchema = z.z.object({
1710
+ positionId: z.z.string().min(1),
1711
+ slug: z.z.string().min(1),
1712
+ label: z.z.string().min(1),
1713
+ role: z.z.string().nullable(),
1714
+ workingDirectoryRelativePath: z.z.string().nullable(),
1715
+ agentId: z.z.string().nullable(),
1716
+ agentName: z.z.string().nullable(),
1717
+ supervisorAgentId: z.z.string().nullable(),
1718
+ responsibilityIds: z.z.array(z.z.string()),
1719
+ occupancyStatus: z.z.enum(["occupied", "vacancy", "assigned", "active"]),
1720
+ createdAt: z.z.number(),
1721
+ updatedAt: z.z.number()
1722
+ });
1723
+ const UpsertAgentRequestSchema = BaseRequestSchema.extend({
1724
+ requestType: z.z.literal("upsert_agent"),
1725
+ agent: AgentUpsertPayloadSchema,
1726
+ position: PositionUpsertPayloadSchema,
1727
+ expectedRevision: z.z.number().nullable()
1728
+ });
1681
1729
  const RequestSchema = z.z.discriminatedUnion("requestType", [
1682
1730
  ReplaceGoalRequestSchema,
1683
1731
  CreateTaskRequestSchema,
1684
- UpdateTaskRequestSchema
1732
+ UpdateTaskRequestSchema,
1733
+ UpsertAgentRequestSchema
1685
1734
  ]);
1686
1735
  const RevisionEnvelopeSchema = (value) => z.z.object({
1687
1736
  schemaVersion: z.z.literal(HAPPY_ORG_LOCAL_REPO_SCHEMA_VERSION),
@@ -1694,16 +1743,23 @@ const PositionValueSchema = z.z.object({
1694
1743
  positionId: z.z.string(),
1695
1744
  slug: z.z.string(),
1696
1745
  label: z.z.string(),
1697
- role: z.z.string(),
1698
- agentId: z.z.string(),
1699
- agentName: z.z.string(),
1746
+ role: z.z.string().nullable().optional(),
1747
+ workingDirectoryRelativePath: z.z.string().nullable().optional(),
1748
+ agentId: z.z.string().nullable().optional(),
1749
+ agentName: z.z.string().nullable().optional(),
1700
1750
  supervisorAgentId: z.z.string().nullable(),
1701
1751
  responsibilityIds: z.z.array(z.z.string()),
1702
- occupancyStatus: z.z.enum(["occupied", "vacancy"]),
1752
+ occupancyStatus: z.z.enum(["occupied", "vacancy", "assigned", "active"]),
1703
1753
  truthPath: z.z.string(),
1704
1754
  createdAt: z.z.number(),
1705
1755
  updatedAt: z.z.number()
1706
1756
  });
1757
+ const WorkspaceValueSchema = z.z.object({
1758
+ positionId: z.z.string(),
1759
+ slug: z.z.string(),
1760
+ relativePath: z.z.string().nullable(),
1761
+ updatedAt: z.z.number()
1762
+ });
1707
1763
  const ResponsibilityValueSchema = z.z.object({
1708
1764
  responsibilityId: z.z.string(),
1709
1765
  title: z.z.string(),
@@ -1713,6 +1769,7 @@ const ResponsibilityValueSchema = z.z.object({
1713
1769
  status: z.z.enum(["vacancy", "assigned", "in_progress"]),
1714
1770
  memberAgentId: z.z.string().nullable(),
1715
1771
  memberName: z.z.string().nullable(),
1772
+ role: z.z.string().nullable().optional(),
1716
1773
  activeTaskId: z.z.string().nullable(),
1717
1774
  createdAt: z.z.number(),
1718
1775
  updatedAt: z.z.number()
@@ -1795,11 +1852,45 @@ const LogValueSchema = z.z.object({
1795
1852
  });
1796
1853
  const GoalEnvelopeSchema = RevisionEnvelopeSchema(GoalSchema);
1797
1854
  const PositionEnvelopeSchema = RevisionEnvelopeSchema(PositionValueSchema);
1855
+ const WorkspaceEnvelopeSchema = RevisionEnvelopeSchema(WorkspaceValueSchema);
1798
1856
  const ResponsibilityEnvelopeSchema = RevisionEnvelopeSchema(z.z.array(ResponsibilityValueSchema));
1799
1857
  const TaskEnvelopeSchema = RevisionEnvelopeSchema(TaskValueSchema);
1800
1858
  const WorkflowEnvelopeSchema = RevisionEnvelopeSchema(WorkflowValueSchema);
1801
1859
  const ResultEnvelopeSchema = RevisionEnvelopeSchema(ResultValueSchema);
1802
1860
  const LogEnvelopeSchema = RevisionEnvelopeSchema(LogValueSchema);
1861
+ const WriterLockRecordSchema = z.z.object({
1862
+ schemaVersion: z.z.literal(HAPPY_ORG_WRITER_LOCK_SCHEMA_VERSION),
1863
+ lockId: z.z.string().min(1),
1864
+ target: z.z.string().min(1),
1865
+ holderType: z.z.literal("cli-writer"),
1866
+ holderId: z.z.string().min(1),
1867
+ requestId: z.z.string().min(1),
1868
+ pid: z.z.number().int().nonnegative(),
1869
+ acquiredAt: z.z.number().int().nonnegative(),
1870
+ renewedAt: z.z.number().int().nonnegative(),
1871
+ expiresAt: z.z.number().int().nonnegative(),
1872
+ leaseDurationSeconds: z.z.literal(WRITER_LOCK_LEASE_DURATION_SECONDS),
1873
+ renewBeforeSeconds: z.z.literal(WRITER_LOCK_RENEW_BEFORE_SECONDS),
1874
+ ceoTakeoverAfterSeconds: z.z.literal(WRITER_LOCK_CEO_TAKEOVER_AFTER_SECONDS),
1875
+ takeoverOf: z.z.object({
1876
+ lockId: z.z.string().min(1),
1877
+ holderId: z.z.string().min(1),
1878
+ requestId: z.z.string().min(1),
1879
+ renewedAt: z.z.number().int().nonnegative()
1880
+ }).nullable()
1881
+ });
1882
+ class WriterLockConflictError extends Error {
1883
+ state;
1884
+ target;
1885
+ existing;
1886
+ constructor(state, target, message, existing = null) {
1887
+ super(message);
1888
+ this.name = "WriterLockConflictError";
1889
+ this.state = state;
1890
+ this.target = target;
1891
+ this.existing = existing;
1892
+ }
1893
+ }
1803
1894
  function normalizePortablePath(value) {
1804
1895
  const trimmed = value?.trim();
1805
1896
  if (!trimmed) {
@@ -1807,14 +1898,37 @@ function normalizePortablePath(value) {
1807
1898
  }
1808
1899
  return trimmed.replace(/\\/g, "/").replace(/\/{2,}/g, "/").replace(/\/$/, "");
1809
1900
  }
1901
+ function normalizeRelativePath(value) {
1902
+ const segments = (value ?? "").split(/[\\/]+/).map((segment) => segment.trim()).filter(Boolean).filter((segment) => segment !== "." && segment !== "..");
1903
+ return segments.length > 0 ? segments.join("/") : null;
1904
+ }
1905
+ function resolveRelativePath(rootPath, relativePath) {
1906
+ const normalizedRelativePath = normalizeRelativePath(relativePath);
1907
+ if (!normalizedRelativePath) {
1908
+ return null;
1909
+ }
1910
+ return path.join(rootPath, ...normalizedRelativePath.split("/"));
1911
+ }
1912
+ function normalizeOccupancyStatus(value) {
1913
+ switch (value) {
1914
+ case "vacancy":
1915
+ return "vacancy";
1916
+ case "active":
1917
+ return "active";
1918
+ case "assigned":
1919
+ case "occupied":
1920
+ default:
1921
+ return "assigned";
1922
+ }
1923
+ }
1810
1924
  function normalizeAdviceRoute(value) {
1811
1925
  return value === "gm" || value === "lead" || value === "user" ? value : null;
1812
1926
  }
1813
1927
  function buildResponsibilityId(slug) {
1814
1928
  return `responsibility-${slug}`;
1815
1929
  }
1816
- function buildPositionId(slug) {
1817
- return `position-${slug}`;
1930
+ function buildPositionRoot(rootPath, slug) {
1931
+ return path.join(rootPath, "positions", slug);
1818
1932
  }
1819
1933
  function toTaskSlug(taskId, title) {
1820
1934
  const slug = title.trim().toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-").replace(/^-+|-+$/g, "");
@@ -1859,6 +1973,74 @@ function buildRequestFilePath(rootPath, requestId) {
1859
1973
  function buildTaskRoot(rootPath, taskId) {
1860
1974
  return path.join(rootPath, "tasks", taskId);
1861
1975
  }
1976
+ function nextWriterLockId() {
1977
+ writerLockSequence += 1;
1978
+ return `writer-lock-${process.pid}-${Date.now().toString(36)}-${writerLockSequence.toString(36)}`;
1979
+ }
1980
+ function normalizeWriterLockTarget(target) {
1981
+ return target.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "writer-lock";
1982
+ }
1983
+ function buildWriterLockPath(rootPath, target) {
1984
+ return path.join(rootPath, ".runtime", "locks", `${normalizeWriterLockTarget(target)}.lock.json`);
1985
+ }
1986
+ function buildWriterLockRecord(params) {
1987
+ const now = params.now ?? Date.now();
1988
+ return {
1989
+ schemaVersion: HAPPY_ORG_WRITER_LOCK_SCHEMA_VERSION,
1990
+ lockId: nextWriterLockId(),
1991
+ target: params.target,
1992
+ holderType: "cli-writer",
1993
+ holderId: params.holderId,
1994
+ requestId: params.requestId,
1995
+ pid: process.pid,
1996
+ acquiredAt: now,
1997
+ renewedAt: now,
1998
+ expiresAt: now + WRITER_LOCK_LEASE_DURATION_SECONDS * 1e3,
1999
+ leaseDurationSeconds: WRITER_LOCK_LEASE_DURATION_SECONDS,
2000
+ renewBeforeSeconds: WRITER_LOCK_RENEW_BEFORE_SECONDS,
2001
+ ceoTakeoverAfterSeconds: WRITER_LOCK_CEO_TAKEOVER_AFTER_SECONDS,
2002
+ takeoverOf: params.takeoverOf ? {
2003
+ lockId: params.takeoverOf.lockId,
2004
+ holderId: params.takeoverOf.holderId,
2005
+ requestId: params.takeoverOf.requestId,
2006
+ renewedAt: params.takeoverOf.renewedAt
2007
+ } : null
2008
+ };
2009
+ }
2010
+ function getWriterLockAgeMs(lock, now) {
2011
+ return Math.max(now - Math.max(lock.renewedAt, lock.acquiredAt), 0);
2012
+ }
2013
+ async function removeWriterLockFile(lockPath) {
2014
+ await promises.unlink(lockPath).catch(async () => {
2015
+ await promises.rm(lockPath, { force: true });
2016
+ });
2017
+ }
2018
+ async function readWriterLockRecord(lockPath) {
2019
+ const payload = await readJson(lockPath);
2020
+ const parsed = WriterLockRecordSchema.safeParse(payload);
2021
+ return parsed.success ? parsed.data : null;
2022
+ }
2023
+ function isFileExistsError(error) {
2024
+ return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
2025
+ }
2026
+ function isFileNotFoundError(error) {
2027
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
2028
+ }
2029
+ function getWriterLockTargetsForRequest(request) {
2030
+ if (request.requestType === "replace_goal") {
2031
+ return ["goal"];
2032
+ }
2033
+ if (request.requestType === "upsert_agent") {
2034
+ return [`position-${request.position.positionId}`, "responsibilities"];
2035
+ }
2036
+ if (request.requestType === "create_task") {
2037
+ return ["task-create", "responsibilities"];
2038
+ }
2039
+ return [`task-${request.taskId}`, "responsibilities"];
2040
+ }
2041
+ function getWriterLockTargetsForTurnReport(taskId) {
2042
+ return [`task-${taskId}`, "responsibilities"];
2043
+ }
1862
2044
  async function loadTaskEnvelope(rootPath, taskId) {
1863
2045
  const taskRoot = buildTaskRoot(rootPath, taskId);
1864
2046
  const task = await readEnvelope(path.join(taskRoot, "TASK.json"), TaskEnvelopeSchema);
@@ -1883,15 +2065,52 @@ async function nextTaskId(rootPath) {
1883
2065
  const nextNumber = taskIds.map((taskId) => Number.parseInt(taskId.replace(/^T-/, ""), 10)).filter((value) => Number.isFinite(value)).reduce((max, value) => Math.max(max, value), 0) + 1;
1884
2066
  return `T-${String(nextNumber).padStart(3, "0")}`;
1885
2067
  }
1886
- async function listPositionEnvelopes(rootPath) {
2068
+ async function listPositionEntries(rootPath) {
1887
2069
  try {
1888
2070
  const entries = await promises.readdir(path.join(rootPath, "positions"), { withFileTypes: true });
1889
- const envelopes = await Promise.all(entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).map((entry) => readEnvelope(path.join(rootPath, "positions", entry.name), PositionEnvelopeSchema)));
1890
- return envelopes.filter((entry) => entry !== null);
2071
+ const positionEntries = await Promise.all(entries.map(async (entry) => {
2072
+ if (entry.isDirectory()) {
2073
+ const positionRoot = path.join(rootPath, "positions", entry.name);
2074
+ const position = await readEnvelope(path.join(positionRoot, "POSITION.json"), PositionEnvelopeSchema);
2075
+ if (!position) {
2076
+ return null;
2077
+ }
2078
+ const workspace = await readEnvelope(path.join(positionRoot, "WORKSPACE.json"), WorkspaceEnvelopeSchema);
2079
+ return {
2080
+ position,
2081
+ workspace,
2082
+ source: "directory"
2083
+ };
2084
+ }
2085
+ if (entry.isFile() && entry.name.endsWith(".json")) {
2086
+ const position = await readEnvelope(path.join(rootPath, "positions", entry.name), PositionEnvelopeSchema);
2087
+ if (!position) {
2088
+ return null;
2089
+ }
2090
+ return {
2091
+ position,
2092
+ workspace: null,
2093
+ source: "legacy-file"
2094
+ };
2095
+ }
2096
+ return null;
2097
+ }));
2098
+ const deduped = /* @__PURE__ */ new Map();
2099
+ for (const entry of positionEntries.filter((value) => value !== null)) {
2100
+ const key = entry.position.value.positionId;
2101
+ const existing = deduped.get(key) ?? null;
2102
+ if (!existing || existing.source === "legacy-file" && entry.source === "directory") {
2103
+ deduped.set(key, entry);
2104
+ }
2105
+ }
2106
+ return [...deduped.values()];
1891
2107
  } catch {
1892
2108
  return [];
1893
2109
  }
1894
2110
  }
2111
+ async function listPositionEnvelopes(rootPath) {
2112
+ return (await listPositionEntries(rootPath)).map((entry) => entry.position);
2113
+ }
1895
2114
  async function readResponsibilities(rootPath) {
1896
2115
  return await readEnvelope(path.join(rootPath, "RESPONSIBILITIES.json"), ResponsibilityEnvelopeSchema);
1897
2116
  }
@@ -1924,16 +2143,18 @@ async function rebuildResponsibilities(rootPath) {
1924
2143
  const existing = await readResponsibilities(rootPath);
1925
2144
  const previousRevision = existing?.revision ?? 0;
1926
2145
  const responsibilities = positions.map((position) => {
1927
- const activeTask = normalizedTasks.filter((task) => task.ownerAgentId === position.value.agentId && task.status !== "done" && task.status !== "terminated").sort((left, right) => right.updatedAt - left.updatedAt)[0] ?? null;
2146
+ const responsibilityId = position.value.responsibilityIds[0] ?? buildResponsibilityId(position.value.slug);
2147
+ const activeTask = normalizedTasks.filter((task) => task.status !== "done" && task.status !== "terminated").filter((task) => task.responsibilityId === responsibilityId || (position.value.agentId ? task.ownerAgentId === position.value.agentId : false)).sort((left, right) => right.updatedAt - left.updatedAt)[0] ?? null;
1928
2148
  return {
1929
- responsibilityId: buildResponsibilityId(position.value.slug),
1930
- title: `${position.value.agentName} responsibility`,
2149
+ responsibilityId,
2150
+ title: position.value.label || position.value.agentName || `${position.value.slug} responsibility`,
1931
2151
  summary: activeTask?.summary ?? null,
1932
- positionId: buildPositionId(position.value.slug),
1933
- participantType: "agent",
1934
- status: activeTask ? "in_progress" : "assigned",
1935
- memberAgentId: position.value.agentId,
1936
- memberName: position.value.agentName,
2152
+ positionId: position.value.positionId,
2153
+ participantType: position.value.agentId ? "agent" : "vacancy",
2154
+ status: position.value.agentId ? activeTask ? "in_progress" : "assigned" : "vacancy",
2155
+ memberAgentId: position.value.agentId ?? null,
2156
+ memberName: position.value.agentName ?? null,
2157
+ role: position.value.role ?? null,
1937
2158
  activeTaskId: activeTask?.taskId ?? null,
1938
2159
  createdAt: position.value.createdAt,
1939
2160
  updatedAt: Math.max(position.value.updatedAt, activeTask?.updatedAt ?? 0)
@@ -2004,12 +2225,104 @@ async function applyReplaceGoalRequest(rootPath, request) {
2004
2225
  message: "goal updated"
2005
2226
  };
2006
2227
  }
2228
+ async function applyUpsertAgentRequest(rootPath, request) {
2229
+ const positionEntries = await listPositionEntries(rootPath);
2230
+ const existingPosition = positionEntries.find((entry) => entry.position.value.positionId === request.position.positionId || entry.position.value.slug === request.position.slug || entry.position.value.agentId === request.agent.id) ?? null;
2231
+ if (request.expectedRevision !== null && existingPosition?.position.revision !== request.expectedRevision) {
2232
+ const message = existingPosition ? `Position revision mismatch: expected ${request.expectedRevision}, got ${existingPosition.position.revision}` : `Position ${request.position.positionId} not found for expected revision ${request.expectedRevision}`;
2233
+ await updateRequestRecord(rootPath, request, {
2234
+ requestState: "recovery_required",
2235
+ processedAt: Date.now(),
2236
+ resultMessage: message
2237
+ });
2238
+ return {
2239
+ outcome: "recovery_required",
2240
+ requestId: request.requestId,
2241
+ taskId: null,
2242
+ message
2243
+ };
2244
+ }
2245
+ const positionRoot = buildPositionRoot(rootPath, request.position.slug);
2246
+ const now = Date.now();
2247
+ const positionValue = {
2248
+ positionId: request.position.positionId,
2249
+ slug: request.position.slug,
2250
+ label: request.position.label,
2251
+ role: request.position.role ?? request.agent.role,
2252
+ workingDirectoryRelativePath: normalizeRelativePath(
2253
+ request.position.workingDirectoryRelativePath ?? request.agent.workingDirectoryRelativePath
2254
+ ),
2255
+ agentId: request.position.agentId ?? request.agent.id,
2256
+ agentName: request.position.agentName ?? request.agent.name,
2257
+ supervisorAgentId: request.position.supervisorAgentId ?? request.agent.supervisorAgentId,
2258
+ responsibilityIds: request.position.responsibilityIds.length > 0 ? request.position.responsibilityIds : [buildResponsibilityId(request.position.slug)],
2259
+ occupancyStatus: normalizeOccupancyStatus(request.position.occupancyStatus),
2260
+ truthPath: `positions/${request.position.slug}/POSITION.json`,
2261
+ createdAt: request.position.createdAt,
2262
+ updatedAt: request.agent.updatedAt
2263
+ };
2264
+ const existingWorkspace = existingPosition?.workspace ?? null;
2265
+ const workspaceValue = {
2266
+ positionId: request.position.positionId,
2267
+ slug: request.position.slug,
2268
+ relativePath: normalizeRelativePath(
2269
+ request.position.workingDirectoryRelativePath ?? request.agent.workingDirectoryRelativePath
2270
+ ),
2271
+ updatedAt: request.agent.updatedAt
2272
+ };
2273
+ const runtimeValue = {
2274
+ agentId: request.agent.id,
2275
+ slug: request.agent.slug,
2276
+ role: request.agent.role,
2277
+ sessionId: request.agent.sessionId,
2278
+ sessionTitle: request.agent.sessionTitle,
2279
+ runtimePath: request.agent.runtimePath,
2280
+ assignedWorkingDirectoryRelativePath: workspaceValue.relativePath,
2281
+ assignedWorkingDirectoryPath: resolveRelativePath(rootPath, workspaceValue.relativePath),
2282
+ machineId: request.agent.machineId,
2283
+ backend: request.agent.backend,
2284
+ updatedAt: request.agent.updatedAt
2285
+ };
2286
+ await ensureDir(positionRoot);
2287
+ await writeEnvelope(path.join(positionRoot, "POSITION.json"), {
2288
+ schemaVersion: HAPPY_ORG_LOCAL_REPO_SCHEMA_VERSION,
2289
+ revision: existingPosition && JSON.stringify(existingPosition.position.value) === JSON.stringify(positionValue) ? existingPosition.position.revision : (existingPosition?.position.revision ?? 0) + 1,
2290
+ updatedAt: now,
2291
+ updatedBy: request.agent.id,
2292
+ value: positionValue
2293
+ });
2294
+ await writeEnvelope(path.join(positionRoot, "WORKSPACE.json"), {
2295
+ schemaVersion: HAPPY_ORG_LOCAL_REPO_SCHEMA_VERSION,
2296
+ revision: existingWorkspace && JSON.stringify(existingWorkspace.value) === JSON.stringify(workspaceValue) ? existingWorkspace.revision : (existingWorkspace?.revision ?? 0) + 1,
2297
+ updatedAt: now,
2298
+ updatedBy: request.agent.id,
2299
+ value: workspaceValue
2300
+ });
2301
+ await writeJson(path.join(rootPath, ".runtime", "agents", `${request.agent.slug}.json`), runtimeValue);
2302
+ if (existingPosition?.source === "legacy-file") {
2303
+ await promises.rm(path.join(rootPath, "positions", `${existingPosition.position.value.positionId}.json`), { force: true });
2304
+ }
2305
+ await rebuildResponsibilities(rootPath);
2306
+ await updateRequestRecord(rootPath, request, {
2307
+ requestState: "applied",
2308
+ processedAt: now,
2309
+ resultMessage: `upserted ${request.agent.id}`
2310
+ });
2311
+ return {
2312
+ outcome: "applied",
2313
+ requestId: request.requestId,
2314
+ taskId: null,
2315
+ message: `upserted ${request.agent.id}`
2316
+ };
2317
+ }
2007
2318
  async function applyCreateTaskRequest(rootPath, request) {
2008
2319
  const taskId = await nextTaskId(rootPath);
2009
2320
  const taskRoot = buildTaskRoot(rootPath, taskId);
2010
2321
  const createdAt = Date.now();
2011
2322
  const title = request.title.trim();
2012
2323
  const position = (await listPositionEnvelopes(rootPath)).find((entry) => entry.value.agentId === request.ownerAgentId) ?? null;
2324
+ const responsibilityId = position?.value.responsibilityIds[0] ?? (position ? buildResponsibilityId(position.value.slug) : null);
2325
+ const responsibilityLabel = position?.value.label ?? `${request.ownerName} responsibility`;
2013
2326
  const taskValue = {
2014
2327
  taskId,
2015
2328
  slug: toTaskSlug(taskId, title),
@@ -2017,8 +2330,8 @@ async function applyCreateTaskRequest(rootPath, request) {
2017
2330
  ownerAgentId: request.ownerAgentId,
2018
2331
  ownerName: request.ownerName,
2019
2332
  ownerSessionId: request.ownerSessionId,
2020
- responsibilityId: position ? buildResponsibilityId(position.value.slug) : null,
2021
- responsibilityLabel: position ? `${position.value.agentName} responsibility` : `${request.ownerName} responsibility`,
2333
+ responsibilityId,
2334
+ responsibilityLabel,
2022
2335
  activeOwnerAgentId: null,
2023
2336
  hasActiveOwner: false,
2024
2337
  path: request.path,
@@ -2236,19 +2549,127 @@ async function applyUpdateTaskRequest(rootPath, request) {
2236
2549
  message: `updated ${request.taskId}`
2237
2550
  };
2238
2551
  }
2239
- async function acquireWriterLock(rootPath) {
2240
- const lockPath = path.join(rootPath, ".runtime", "writer.lock.json");
2552
+ async function renewSingleWriterLock(lockPath, lockId) {
2553
+ const existing = await readWriterLockRecord(lockPath);
2554
+ if (!existing || existing.lockId !== lockId) {
2555
+ return;
2556
+ }
2557
+ const now = Date.now();
2558
+ await writeJson(lockPath, {
2559
+ ...existing,
2560
+ renewedAt: now,
2561
+ expiresAt: now + WRITER_LOCK_LEASE_DURATION_SECONDS * 1e3
2562
+ });
2563
+ }
2564
+ async function acquireSingleWriterLock(params) {
2565
+ const lockPath = buildWriterLockPath(params.rootPath, params.target);
2241
2566
  await ensureDir(path.dirname(lockPath));
2242
- const handle = await promises.open(lockPath, "wx");
2243
- await handle.writeFile(JSON.stringify({
2244
- acquiredAt: Date.now(),
2245
- pid: process.pid
2246
- }, null, 2), "utf8");
2247
- await handle.close();
2248
- return async () => {
2249
- await promises.unlink(lockPath).catch(async () => {
2250
- await promises.rm(lockPath, { force: true });
2567
+ let takeoverOf = null;
2568
+ while (true) {
2569
+ const now = Date.now();
2570
+ const record = buildWriterLockRecord({
2571
+ target: params.target,
2572
+ holderId: params.holderId,
2573
+ requestId: params.requestId,
2574
+ takeoverOf,
2575
+ now
2251
2576
  });
2577
+ try {
2578
+ const handle = await promises.open(lockPath, "wx");
2579
+ await handle.writeFile(JSON.stringify(record, null, 2), "utf8");
2580
+ await handle.close();
2581
+ const renewTimer = setInterval(() => {
2582
+ void renewSingleWriterLock(lockPath, record.lockId);
2583
+ }, WRITER_LOCK_RENEW_INTERVAL_MS);
2584
+ renewTimer.unref?.();
2585
+ return async () => {
2586
+ clearInterval(renewTimer);
2587
+ const existing = await readWriterLockRecord(lockPath);
2588
+ if (!existing || existing.lockId !== record.lockId) {
2589
+ return;
2590
+ }
2591
+ await removeWriterLockFile(lockPath);
2592
+ };
2593
+ } catch (error) {
2594
+ if (!isFileExistsError(error)) {
2595
+ throw error;
2596
+ }
2597
+ const existing = await readWriterLockRecord(lockPath);
2598
+ if (!existing) {
2599
+ try {
2600
+ const info = await promises.stat(lockPath);
2601
+ const ageMs2 = Math.max(now - info.mtimeMs, 0);
2602
+ if (ageMs2 >= WRITER_LOCK_CEO_TAKEOVER_AFTER_SECONDS * 1e3) {
2603
+ await removeWriterLockFile(lockPath);
2604
+ takeoverOf = null;
2605
+ continue;
2606
+ }
2607
+ } catch (statError) {
2608
+ if (isFileNotFoundError(statError)) {
2609
+ takeoverOf = null;
2610
+ continue;
2611
+ }
2612
+ }
2613
+ throw new WriterLockConflictError(
2614
+ "invalid",
2615
+ params.target,
2616
+ `Lock ${params.target} exists but is unreadable; mark recovery before retrying.`
2617
+ );
2618
+ }
2619
+ const ageMs = getWriterLockAgeMs(existing, now);
2620
+ if (ageMs >= WRITER_LOCK_CEO_TAKEOVER_AFTER_SECONDS * 1e3) {
2621
+ await removeWriterLockFile(lockPath);
2622
+ takeoverOf = existing;
2623
+ continue;
2624
+ }
2625
+ if (ageMs >= WRITER_LOCK_LEASE_DURATION_SECONDS * 1e3) {
2626
+ throw new WriterLockConflictError(
2627
+ "stale",
2628
+ params.target,
2629
+ `Lock ${params.target} is stale for ${Math.floor(ageMs / 1e3)}s and requires CEO recovery before takeover.`,
2630
+ existing
2631
+ );
2632
+ }
2633
+ throw new WriterLockConflictError(
2634
+ "active",
2635
+ params.target,
2636
+ `Lock ${params.target} is still held by ${existing.holderId} for request ${existing.requestId}.`,
2637
+ existing
2638
+ );
2639
+ }
2640
+ }
2641
+ }
2642
+ async function acquireWriterLocks(params) {
2643
+ const holderId = `cli-writer:${process.pid}`;
2644
+ const releaseSteps = [];
2645
+ const orderedTargets = [...new Set(params.targets)].sort();
2646
+ try {
2647
+ for (const target of orderedTargets) {
2648
+ releaseSteps.push(await acquireSingleWriterLock({
2649
+ rootPath: params.rootPath,
2650
+ target,
2651
+ holderId,
2652
+ requestId: params.requestId
2653
+ }));
2654
+ }
2655
+ } catch (error) {
2656
+ while (releaseSteps.length > 0) {
2657
+ const release = releaseSteps.pop();
2658
+ if (!release) {
2659
+ continue;
2660
+ }
2661
+ await release();
2662
+ }
2663
+ throw error;
2664
+ }
2665
+ return async () => {
2666
+ while (releaseSteps.length > 0) {
2667
+ const release = releaseSteps.pop();
2668
+ if (!release) {
2669
+ continue;
2670
+ }
2671
+ await release();
2672
+ }
2252
2673
  };
2253
2674
  }
2254
2675
  async function processSingleRequest(rootPath, request) {
@@ -2261,11 +2682,38 @@ async function processSingleRequest(rootPath, request) {
2261
2682
  message: request.resultMessage
2262
2683
  };
2263
2684
  }
2264
- const release = await acquireWriterLock(rootPath);
2685
+ let lockConflictMessage = null;
2686
+ const release = await acquireWriterLocks({
2687
+ rootPath,
2688
+ targets: getWriterLockTargetsForRequest(request),
2689
+ requestId: request.requestId
2690
+ }).catch(async (error) => {
2691
+ if (error instanceof WriterLockConflictError) {
2692
+ lockConflictMessage = error.message;
2693
+ await updateRequestRecord(rootPath, request, {
2694
+ requestState: "recovery_required",
2695
+ processedAt: Date.now(),
2696
+ resultMessage: error.message
2697
+ });
2698
+ return null;
2699
+ }
2700
+ throw error;
2701
+ });
2702
+ if (!release) {
2703
+ return {
2704
+ outcome: "recovery_required",
2705
+ requestId: request.requestId,
2706
+ taskId: "taskId" in request ? request.taskId : null,
2707
+ message: lockConflictMessage
2708
+ };
2709
+ }
2265
2710
  try {
2266
2711
  if (request.requestType === "replace_goal") {
2267
2712
  return await applyReplaceGoalRequest(rootPath, request);
2268
2713
  }
2714
+ if (request.requestType === "upsert_agent") {
2715
+ return await applyUpsertAgentRequest(rootPath, request);
2716
+ }
2269
2717
  if (request.requestType === "create_task") {
2270
2718
  return await applyCreateTaskRequest(rootPath, request);
2271
2719
  }
@@ -2306,99 +2754,125 @@ function nextStatusFromTurnReport(report) {
2306
2754
  return "active";
2307
2755
  }
2308
2756
  async function recordHappyOrgTurnReport(options) {
2309
- const bundle = await loadTaskEnvelope(options.rootPath, options.report.taskId);
2310
- if (!bundle) {
2757
+ const requestId = `turn-report:${options.report.taskId}`;
2758
+ let lockConflictMessage = null;
2759
+ const release = await acquireWriterLocks({
2760
+ rootPath: options.rootPath,
2761
+ targets: getWriterLockTargetsForTurnReport(options.report.taskId),
2762
+ requestId
2763
+ }).catch((error) => {
2764
+ if (error instanceof WriterLockConflictError) {
2765
+ lockConflictMessage = error.message;
2766
+ return null;
2767
+ }
2768
+ throw error;
2769
+ });
2770
+ if (!release) {
2311
2771
  return {
2312
2772
  outcome: "recovery_required",
2313
- requestId: `turn-report:${options.report.taskId}`,
2773
+ requestId,
2314
2774
  taskId: options.report.taskId,
2315
- message: `Task ${options.report.taskId} not found`
2775
+ message: lockConflictMessage ?? `Turn report blocked by writer lock for ${options.report.taskId}`
2316
2776
  };
2317
2777
  }
2318
- const now = Date.now();
2319
- const nextStatus = nextStatusFromTurnReport(options.report);
2320
- const nextTaskValue = {
2321
- ...bundle.task.value,
2322
- status: nextStatus,
2323
- summary: options.report.summary,
2324
- resultSummary: options.report.turnStatus === "task_complete" ? options.report.summary : bundle.task.value.resultSummary,
2325
- blocker: nextStatus === "blocked" ? options.report.summary || options.report.blockerCode : null,
2326
- decisionNeeded: nextStatus === "waiting_decision" ? options.report.decisionNeeded ?? options.report.summary : null,
2327
- updatedAt: now,
2328
- lastUpdatedBy: options.report.memberAgentId,
2329
- activeOwnerAgentId: options.report.memberAgentId,
2330
- hasActiveOwner: nextStatus === "active",
2331
- path: options.report.targetArtifact ?? bundle.task.value.path,
2332
- portablePath: normalizePortablePath(options.report.targetArtifact ?? bundle.task.value.path)
2333
- };
2334
- await writeEnvelope(path.join(buildTaskRoot(options.rootPath, options.report.taskId), "TASK.json"), {
2335
- ...bundle.task,
2336
- revision: bundle.task.revision + 1,
2337
- updatedAt: now,
2338
- updatedBy: options.report.memberAgentId,
2339
- value: nextTaskValue
2340
- });
2341
- await writeEnvelope(path.join(buildTaskRoot(options.rootPath, options.report.taskId), "WORKFLOW.json"), {
2342
- schemaVersion: HAPPY_ORG_LOCAL_REPO_SCHEMA_VERSION,
2343
- revision: (bundle.workflow?.revision ?? 0) + 1,
2344
- updatedAt: now,
2345
- updatedBy: options.report.memberAgentId,
2346
- value: {
2347
- taskId: options.report.taskId,
2778
+ try {
2779
+ const bundle = await loadTaskEnvelope(options.rootPath, options.report.taskId);
2780
+ if (!bundle) {
2781
+ return {
2782
+ outcome: "recovery_required",
2783
+ requestId,
2784
+ taskId: options.report.taskId,
2785
+ message: `Task ${options.report.taskId} not found`
2786
+ };
2787
+ }
2788
+ const now = Date.now();
2789
+ const nextStatus = nextStatusFromTurnReport(options.report);
2790
+ const nextTaskValue = {
2791
+ ...bundle.task.value,
2348
2792
  status: nextStatus,
2349
2793
  summary: options.report.summary,
2350
- blocker: nextStatus === "blocked" ? options.report.blockerCode ?? options.report.summary : null,
2794
+ resultSummary: options.report.turnStatus === "task_complete" ? options.report.summary : bundle.task.value.resultSummary,
2795
+ blocker: nextStatus === "blocked" ? options.report.summary || options.report.blockerCode : null,
2351
2796
  decisionNeeded: nextStatus === "waiting_decision" ? options.report.decisionNeeded ?? options.report.summary : null,
2352
- adviceRoute: null,
2353
- adviceSummary: null,
2354
- lastRequestId: null,
2355
2797
  updatedAt: now,
2356
- updatedBy: options.report.memberAgentId
2357
- }
2358
- });
2359
- await writeEnvelope(path.join(buildTaskRoot(options.rootPath, options.report.taskId), "RESULT.json"), {
2360
- schemaVersion: HAPPY_ORG_LOCAL_REPO_SCHEMA_VERSION,
2361
- revision: (bundle.result?.revision ?? 0) + 1,
2362
- updatedAt: now,
2363
- updatedBy: options.report.memberAgentId,
2364
- value: {
2365
- taskId: options.report.taskId,
2366
- resultSummary: options.report.turnStatus === "task_complete" ? options.report.summary : bundle.result?.value.resultSummary ?? bundle.task.value.resultSummary,
2367
- reviewSummary: bundle.result?.value.reviewSummary ?? null,
2368
- assetCaptureStatus: bundle.result?.value.assetCaptureStatus ?? null,
2369
- assetCaptureSummary: bundle.result?.value.assetCaptureSummary ?? null,
2370
- assetReference: options.report.targetArtifact ?? bundle.result?.value.assetReference ?? null,
2371
- closedAt: nextStatus === "waiting_close" ? now : bundle.result?.value.closedAt ?? null,
2798
+ lastUpdatedBy: options.report.memberAgentId,
2799
+ activeOwnerAgentId: options.report.memberAgentId,
2800
+ hasActiveOwner: nextStatus === "active",
2801
+ path: options.report.targetArtifact ?? bundle.task.value.path,
2802
+ portablePath: normalizePortablePath(options.report.targetArtifact ?? bundle.task.value.path)
2803
+ };
2804
+ const taskRoot = buildTaskRoot(options.rootPath, options.report.taskId);
2805
+ await writeEnvelope(path.join(taskRoot, "TASK.json"), {
2806
+ ...bundle.task,
2807
+ revision: bundle.task.revision + 1,
2372
2808
  updatedAt: now,
2373
- updatedBy: options.report.memberAgentId
2374
- }
2375
- });
2376
- await writeEnvelope(path.join(buildTaskRoot(options.rootPath, options.report.taskId), "LOG.json"), {
2377
- schemaVersion: HAPPY_ORG_LOCAL_REPO_SCHEMA_VERSION,
2378
- revision: (bundle.log?.revision ?? 0) + 1,
2379
- updatedAt: now,
2380
- updatedBy: options.report.memberAgentId,
2381
- value: {
2809
+ updatedBy: options.report.memberAgentId,
2810
+ value: nextTaskValue
2811
+ });
2812
+ await writeEnvelope(path.join(taskRoot, "WORKFLOW.json"), {
2813
+ schemaVersion: HAPPY_ORG_LOCAL_REPO_SCHEMA_VERSION,
2814
+ revision: (bundle.workflow?.revision ?? 0) + 1,
2815
+ updatedAt: now,
2816
+ updatedBy: options.report.memberAgentId,
2817
+ value: {
2818
+ taskId: options.report.taskId,
2819
+ status: nextStatus,
2820
+ summary: options.report.summary,
2821
+ blocker: nextStatus === "blocked" ? options.report.blockerCode ?? options.report.summary : null,
2822
+ decisionNeeded: nextStatus === "waiting_decision" ? options.report.decisionNeeded ?? options.report.summary : null,
2823
+ adviceRoute: null,
2824
+ adviceSummary: null,
2825
+ lastRequestId: null,
2826
+ updatedAt: now,
2827
+ updatedBy: options.report.memberAgentId
2828
+ }
2829
+ });
2830
+ await writeEnvelope(path.join(taskRoot, "RESULT.json"), {
2831
+ schemaVersion: HAPPY_ORG_LOCAL_REPO_SCHEMA_VERSION,
2832
+ revision: (bundle.result?.revision ?? 0) + 1,
2833
+ updatedAt: now,
2834
+ updatedBy: options.report.memberAgentId,
2835
+ value: {
2836
+ taskId: options.report.taskId,
2837
+ resultSummary: options.report.turnStatus === "task_complete" ? options.report.summary : bundle.result?.value.resultSummary ?? bundle.task.value.resultSummary,
2838
+ reviewSummary: bundle.result?.value.reviewSummary ?? null,
2839
+ assetCaptureStatus: bundle.result?.value.assetCaptureStatus ?? null,
2840
+ assetCaptureSummary: bundle.result?.value.assetCaptureSummary ?? null,
2841
+ assetReference: options.report.targetArtifact ?? bundle.result?.value.assetReference ?? null,
2842
+ closedAt: nextStatus === "waiting_close" ? now : bundle.result?.value.closedAt ?? null,
2843
+ updatedAt: now,
2844
+ updatedBy: options.report.memberAgentId
2845
+ }
2846
+ });
2847
+ await writeEnvelope(path.join(taskRoot, "LOG.json"), {
2848
+ schemaVersion: HAPPY_ORG_LOCAL_REPO_SCHEMA_VERSION,
2849
+ revision: (bundle.log?.revision ?? 0) + 1,
2850
+ updatedAt: now,
2851
+ updatedBy: options.report.memberAgentId,
2852
+ value: {
2853
+ taskId: options.report.taskId,
2854
+ entries: [
2855
+ ...bundle.log?.value.entries ?? [],
2856
+ buildTaskLogEntry(
2857
+ "turn_report",
2858
+ `Recorded ${options.report.turnStatus} from ${options.report.memberAgentId}`,
2859
+ options.report.summary,
2860
+ options.report.memberAgentId,
2861
+ now
2862
+ )
2863
+ ]
2864
+ }
2865
+ });
2866
+ await rebuildResponsibilities(options.rootPath);
2867
+ return {
2868
+ outcome: "applied",
2869
+ requestId,
2382
2870
  taskId: options.report.taskId,
2383
- entries: [
2384
- ...bundle.log?.value.entries ?? [],
2385
- buildTaskLogEntry(
2386
- "turn_report",
2387
- `Recorded ${options.report.turnStatus} from ${options.report.memberAgentId}`,
2388
- options.report.summary,
2389
- options.report.memberAgentId,
2390
- now
2391
- )
2392
- ]
2393
- }
2394
- });
2395
- await rebuildResponsibilities(options.rootPath);
2396
- return {
2397
- outcome: "applied",
2398
- requestId: `turn-report:${options.report.taskId}`,
2399
- taskId: options.report.taskId,
2400
- message: `recorded turn report for ${options.report.taskId}`
2401
- };
2871
+ message: `recorded turn report for ${options.report.taskId}`
2872
+ };
2873
+ } finally {
2874
+ await release();
2875
+ }
2402
2876
  }
2403
2877
 
2404
2878
  function buildSessionRuntimeIndex(metadata) {
@@ -3346,6 +3820,9 @@ const PROTOCOL_V3_CHANGES_PAGE_LIMIT = 200;
3346
3820
  const PROTOCOL_V3_CAPABILITIES_WAIT_MS = 250;
3347
3821
  const COMMITTED_SESSION_WRITE_ACK_TIMEOUT_MS = 5e3;
3348
3822
  const RELIABLE_SESSION_MESSAGE_RETRY_DELAY_MS = 250;
3823
+ const LEGACY_CONTROL_ACK_HEADER$1 = Object.freeze({
3824
+ "x-happy-legacy-control-ack": "happy-org-1.7.3-legacy-write"
3825
+ });
3349
3826
  function normalizeHappyOrgOptionalText(value) {
3350
3827
  if (typeof value !== "string") {
3351
3828
  return null;
@@ -3383,6 +3860,12 @@ function buildHappyOrgTurnReportSubmissionKey(messageId, report) {
3383
3860
  report.summary.trim()
3384
3861
  ].join(":");
3385
3862
  }
3863
+ function withLegacyControlAck$1(headers) {
3864
+ return {
3865
+ ...headers ?? {},
3866
+ ...LEGACY_CONTROL_ACK_HEADER$1
3867
+ };
3868
+ }
3386
3869
  function parseHappyOrgFallbackEnvelope(rawEnvelope) {
3387
3870
  try {
3388
3871
  const parsed = JSON.parse(rawEnvelope);
@@ -3403,7 +3886,9 @@ function parseHappyOrgFallbackEnvelope(rawEnvelope) {
3403
3886
  replyTo,
3404
3887
  organizationId: typeof parsed.organization_id === "string" ? parsed.organization_id.trim() : null,
3405
3888
  memberAgentId: typeof parsed.member_agent_id === "string" ? parsed.member_agent_id.trim() : null,
3406
- supervisorAgentId: typeof parsed.supervisor_agent_id === "string" ? parsed.supervisor_agent_id.trim() : null
3889
+ supervisorAgentId: typeof parsed.supervisor_agent_id === "string" ? parsed.supervisor_agent_id.trim() : null,
3890
+ positionId: typeof parsed.position_id === "string" ? parsed.position_id.trim() : null,
3891
+ responsibilityId: typeof parsed.responsibility_id === "string" ? parsed.responsibility_id.trim() : null
3407
3892
  };
3408
3893
  } catch {
3409
3894
  return null;
@@ -3420,13 +3905,17 @@ function buildHappyOrgFallbackUserMessage(rawEnvelope) {
3420
3905
  taskId: envelope.taskId,
3421
3906
  organizationId: envelope.organizationId,
3422
3907
  memberAgentId: envelope.memberAgentId,
3423
- supervisorAgentId: envelope.supervisorAgentId
3908
+ supervisorAgentId: envelope.supervisorAgentId,
3909
+ ...envelope.positionId ? { positionId: envelope.positionId } : {},
3910
+ ...envelope.responsibilityId ? { responsibilityId: envelope.responsibilityId } : {}
3424
3911
  }
3425
3912
  } : {},
3426
3913
  replyContext: {
3427
3914
  dispatchId: envelope.dispatchId,
3428
3915
  scope: envelope.scope,
3429
- replyTo: envelope.replyTo
3916
+ replyTo: envelope.replyTo,
3917
+ ...envelope.positionId ? { positionId: envelope.positionId } : {},
3918
+ ...envelope.responsibilityId ? { responsibilityId: envelope.responsibilityId } : {}
3430
3919
  }
3431
3920
  };
3432
3921
  return {
@@ -4215,10 +4704,10 @@ class ApiSessionClient extends node_events.EventEmitter {
4215
4704
  url,
4216
4705
  body: payload,
4217
4706
  signRequest: true,
4218
- headers: {
4707
+ headers: withLegacyControlAck$1({
4219
4708
  "Content-Type": "application/json",
4220
4709
  "x-happy-organization-id": report.organizationId
4221
- }
4710
+ })
4222
4711
  }),
4223
4712
  timeout: 1e4
4224
4713
  });
@@ -4318,10 +4807,10 @@ class ApiSessionClient extends node_events.EventEmitter {
4318
4807
  url,
4319
4808
  body: payload,
4320
4809
  signRequest: true,
4321
- headers: {
4810
+ headers: withLegacyControlAck$1({
4322
4811
  "Content-Type": "application/json",
4323
4812
  "x-happy-organization-id": candidate.organizationId
4324
- }
4813
+ })
4325
4814
  }),
4326
4815
  timeout: 1e4
4327
4816
  });
@@ -5560,6 +6049,15 @@ async function deriveKey(master, usage, path) {
5560
6049
  }
5561
6050
 
5562
6051
  const SESSION_DATA_KEY_USAGE = "Happy Session Data Key";
6052
+ const LEGACY_CONTROL_ACK_HEADER = Object.freeze({
6053
+ "x-happy-legacy-control-ack": "happy-org-1.7.3-legacy-write"
6054
+ });
6055
+ function withLegacyControlAck(headers) {
6056
+ return {
6057
+ ...headers ?? {},
6058
+ ...LEGACY_CONTROL_ACK_HEADER
6059
+ };
6060
+ }
5563
6061
  class ApiClient {
5564
6062
  static async create(credential) {
5565
6063
  return new ApiClient(credential);
@@ -5656,9 +6154,11 @@ class ApiClient {
5656
6154
  allowed_paths: opts.allowedPaths,
5657
6155
  forbidden_paths: opts.forbiddenPaths ?? [],
5658
6156
  cross_scope_policy: "ask-first",
6157
+ position_id: opts.positionId ?? null,
6158
+ responsibility_id: opts.responsibilityId ?? null,
5659
6159
  note: opts.note ?? null
5660
6160
  },
5661
- headers: { "x-happy-organization-id": opts.organizationId },
6161
+ headers: withLegacyControlAck({ "x-happy-organization-id": opts.organizationId }),
5662
6162
  timeout: 1e4
5663
6163
  });
5664
6164
  return response.data.receipt;
@@ -5684,7 +6184,7 @@ class ApiClient {
5684
6184
  status: opts.status,
5685
6185
  note: opts.note ?? null
5686
6186
  },
5687
- headers: { "x-happy-organization-id": opts.organizationId },
6187
+ headers: withLegacyControlAck({ "x-happy-organization-id": opts.organizationId }),
5688
6188
  timeout: 1e4
5689
6189
  });
5690
6190
  return response.data.receipt;